Files
ai-bot/tests/test_pending_acks.py

130 lines
3.9 KiB
Python

"""Tests for orac.reply_state.PendingAckTable and ReplyCache."""
from __future__ import annotations
import time
from orac.reply_state import PendingAckTable, PendingReply, ReplyCache
def _entry(ack: bytes, peer_pk: bytes, attempt: int = 1) -> PendingReply:
now = time.monotonic()
return PendingReply(
expected_ack=ack,
peer_pk=peer_pk,
peer_name=f"peer{peer_pk[0]:02x}",
text="hello",
first_ts=1234567890,
first_sent_at=now,
attempt=attempt,
max_attempts=5,
retry_at=now + 6.0,
)
def test_register_and_consume_roundtrip() -> None:
table = PendingAckTable(cap=4)
ack = b"\x01\x02\x03\x04"
table.register(_entry(ack, b"\xaa" * 32))
assert table.depth() == 1
entry = table.consume(ack)
assert entry is not None
assert entry.expected_ack == ack
assert table.depth() == 0
# Consuming again returns None.
assert table.consume(ack) is None
def test_expired_returns_entries_past_retry_at() -> None:
table = PendingAckTable()
ack = b"\x00\x00\x00\x01"
e = _entry(ack, b"\x11" * 32)
table.register(e)
# retry_at was set to now + 6.0 — force expiry by passing a far-future "now".
expired = table.expired(time.monotonic() + 10.0)
assert len(expired) == 1
assert expired[0].expected_ack == ack
# expired() does not remove.
assert table.depth() == 1
def test_replace_key_rekeys_entry() -> None:
table = PendingAckTable()
old = b"\x00\x00\x00\x01"
new = b"\x00\x00\x00\x02"
table.register(_entry(old, b"\x22" * 32))
table.replace_key(old, new)
assert table.consume(old) is None
got = table.consume(new)
assert got is not None and got.expected_ack == new
def test_has_pending_for_peer() -> None:
table = PendingAckTable()
pk_a = b"\x10" + b"\x00" * 31
pk_b = b"\x20" + b"\x00" * 31
table.register(_entry(b"\x00\x00\x00\x01", pk_a))
assert table.has_pending_for_peer(pk_a) is True
assert table.has_pending_for_peer(pk_b) is False
def test_cap_evicts_oldest_on_overflow() -> None:
table = PendingAckTable(cap=2)
table.register(_entry(b"\x01\x00\x00\x00", b"\xaa" * 32))
table.register(_entry(b"\x02\x00\x00\x00", b"\xbb" * 32))
table.register(_entry(b"\x03\x00\x00\x00", b"\xcc" * 32))
# Oldest (first registered) should be gone.
assert table.consume(b"\x01\x00\x00\x00") is None
assert table.consume(b"\x02\x00\x00\x00") is not None
assert table.consume(b"\x03\x00\x00\x00") is not None
def test_reply_cache_roundtrip() -> None:
cache = ReplyCache(cap=4, ttl=100.0)
pk = b"\x55" * 32
cache.put(
peer_pk=pk,
peer_name="alice",
dm_text="hi",
reply_packet=b"\xff\xee",
reply_text="hello back",
expected_ack=b"\x01\x02\x03\x04",
)
got = cache.get(pk.hex(), "hi")
assert got is not None and got.reply_text == "hello back"
# Different DM text misses.
assert cache.get(pk.hex(), "different") is None
def test_reply_cache_ttl_expires() -> None:
cache = ReplyCache(cap=4, ttl=0.001)
pk = b"\x66" * 32
cache.put(
peer_pk=pk,
peer_name="bob",
dm_text="q",
reply_packet=b"\x00",
reply_text="a",
expected_ack=b"\x00\x00\x00\x00",
)
time.sleep(0.01)
assert cache.get(pk.hex(), "q") is None
def test_reply_cache_ts_lets_caller_gate_on_age() -> None:
"""Caller can decide whether a hit is within the loss-recovery window."""
cache = ReplyCache(cap=4, ttl=100.0)
pk = b"\x77" * 32
cache.put(
peer_pk=pk,
peer_name="carol",
dm_text="status?",
reply_packet=b"\x00",
reply_text="ok",
expected_ack=b"\x00\x00\x00\x01",
)
got = cache.get(pk.hex(), "status?")
assert got is not None
# Caller interprets .ts; entry is fresh right now.
assert (time.monotonic() - got.ts) < 1.0