b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
102 lines
4.1 KiB
Python
102 lines
4.1 KiB
Python
"""Tests for MessageDeduplicator TTL enforcement (#10306).
|
|
|
|
Previously, is_duplicate() returned True for any previously seen ID without
|
|
checking its age — expired entries were only purged when cache size exceeded
|
|
max_size. Normal workloads never overflowed, so messages stayed "duplicate"
|
|
forever.
|
|
|
|
The fix checks TTL at query time: if the entry's timestamp plus TTL is in
|
|
the past, the entry is treated as expired and the message is allowed through.
|
|
"""
|
|
|
|
import time
|
|
|
|
from gateway.platforms.helpers import MessageDeduplicator
|
|
|
|
|
|
class TestMessageDeduplicatorTTL:
|
|
"""TTL-based expiration must work regardless of cache size."""
|
|
|
|
def test_duplicate_within_ttl(self):
|
|
"""Same message within TTL window is duplicate."""
|
|
dedup = MessageDeduplicator(ttl_seconds=60)
|
|
assert dedup.is_duplicate("msg-1") is False
|
|
assert dedup.is_duplicate("msg-1") is True
|
|
|
|
def test_not_duplicate_after_ttl_expires(self):
|
|
"""Same message AFTER TTL expires should NOT be duplicate."""
|
|
dedup = MessageDeduplicator(ttl_seconds=5)
|
|
assert dedup.is_duplicate("msg-1") is False
|
|
|
|
# Fast-forward time past TTL
|
|
dedup._seen["msg-1"] = time.time() - 10 # 10s ago, TTL is 5s
|
|
assert dedup.is_duplicate("msg-1") is False, \
|
|
"Expired entry should not be treated as duplicate"
|
|
|
|
def test_expired_entry_gets_refreshed(self):
|
|
"""After an expired entry is allowed through, it should be re-tracked."""
|
|
dedup = MessageDeduplicator(ttl_seconds=5)
|
|
assert dedup.is_duplicate("msg-1") is False
|
|
|
|
# Expire the entry
|
|
dedup._seen["msg-1"] = time.time() - 10
|
|
|
|
# Should be allowed through (expired)
|
|
assert dedup.is_duplicate("msg-1") is False
|
|
# Now should be duplicate again (freshly tracked)
|
|
assert dedup.is_duplicate("msg-1") is True
|
|
|
|
def test_different_messages_not_confused(self):
|
|
"""Different message IDs are independent."""
|
|
dedup = MessageDeduplicator(ttl_seconds=60)
|
|
assert dedup.is_duplicate("msg-1") is False
|
|
assert dedup.is_duplicate("msg-2") is False
|
|
assert dedup.is_duplicate("msg-1") is True
|
|
assert dedup.is_duplicate("msg-2") is True
|
|
|
|
def test_empty_id_never_duplicate(self):
|
|
"""Empty/None message IDs are never treated as duplicate."""
|
|
dedup = MessageDeduplicator(ttl_seconds=60)
|
|
assert dedup.is_duplicate("") is False
|
|
assert dedup.is_duplicate("") is False
|
|
|
|
def test_max_size_eviction_prunes_expired(self):
|
|
"""Cache pruning on overflow removes expired entries."""
|
|
dedup = MessageDeduplicator(max_size=5, ttl_seconds=60)
|
|
# Add 6 entries, with the first 3 expired
|
|
now = time.time()
|
|
for i in range(3):
|
|
dedup._seen[f"old-{i}"] = now - 120 # expired (2 min ago, TTL 60s)
|
|
for i in range(3):
|
|
dedup.is_duplicate(f"new-{i}")
|
|
# Now we have 6 entries. Next insert triggers pruning.
|
|
dedup.is_duplicate("trigger")
|
|
# The 3 expired entries should be gone, leaving 4 fresh ones
|
|
assert len(dedup._seen) == 4
|
|
assert "old-0" not in dedup._seen
|
|
assert "new-0" in dedup._seen
|
|
|
|
def test_max_size_eviction_caps_fresh_entries(self):
|
|
"""Fresh entries must still be capped to max_size on overflow."""
|
|
dedup = MessageDeduplicator(max_size=2, ttl_seconds=60)
|
|
|
|
dedup.is_duplicate("msg-1")
|
|
dedup.is_duplicate("msg-2")
|
|
dedup.is_duplicate("msg-3")
|
|
|
|
assert len(dedup._seen) == 2
|
|
assert "msg-1" not in dedup._seen
|
|
assert "msg-2" in dedup._seen
|
|
assert "msg-3" in dedup._seen
|
|
|
|
def test_ttl_zero_means_no_dedup(self):
|
|
"""With TTL=0, all entries expire immediately."""
|
|
dedup = MessageDeduplicator(ttl_seconds=0)
|
|
assert dedup.is_duplicate("msg-1") is False
|
|
# Entry was just added at time.time(), and TTL is 0,
|
|
# so now - seen_time >= 0 = ttl, meaning it's expired
|
|
# But time.time() might be the exact same float, so
|
|
# the check is `now - ts < ttl` which is `0 < 0` = False
|
|
# This means TTL=0 effectively disables dedup
|
|
assert dedup.is_duplicate("msg-1") is False
|