Files
turnstone/tests/test_attachment_buffer.py
T
Patrick Buckley 0c3d1d6cd1 test: satisfy ruff-format and lift side-effects out of asserts
- ruff format on two test modules that had drifted (a stray blank line
  and multi-line calls that now fit on one line) — restores a clean
  `ruff format --check`.
- test_attachment_buffer: pull `buf.discard(...)` out of the `assert`
  expressions into locals so the eviction still runs under `python -O`
  (CodeQL: assert statement has a side effect).
2026-06-04 11:03:13 -07:00

140 lines
5.6 KiB
Python

"""Tests for the per-node pending-upload buffer (turnstone.core.attachment_buffer)."""
from __future__ import annotations
import hashlib
from turnstone.core.attachment_buffer import (
AttachmentBuffer,
StagedAttachment,
get_attachment_buffer,
)
def _stage(
buf: AttachmentBuffer,
*,
content: bytes = b"hi",
ws: str = "ws1",
user: str = "u1",
filename: str = "f.txt",
mime: str = "text/plain",
kind: str = "text",
) -> StagedAttachment:
return buf.stage(
ws_id=ws, user_id=user, filename=filename, mime_type=mime, kind=kind, content=content
)
def test_stage_returns_content_hash_id_and_size() -> None:
buf = AttachmentBuffer()
entry = _stage(buf, content=b"hello")
assert entry.attachment_id == hashlib.sha256(b"hello").hexdigest()
assert entry.size_bytes == 5
def test_stage_is_idempotent_for_identical_bytes() -> None:
buf = AttachmentBuffer()
a = _stage(buf, content=b"same")
b = _stage(buf, content=b"same")
assert a.attachment_id == b.attachment_id
assert len(buf.list_for(ws_id="ws1", user_id="u1")) == 1 # deduped by content hash
def test_get_enforces_scope() -> None:
buf = AttachmentBuffer()
entry = _stage(buf, ws="ws1", user="u1")
assert buf.get(entry.attachment_id, ws_id="ws1", user_id="u1") is not None
assert buf.get(entry.attachment_id, ws_id="ws2", user_id="u1") is None # wrong ws
assert buf.get(entry.attachment_id, ws_id="ws1", user_id="u2") is None # wrong user
def test_list_for_scopes_by_ws_and_user() -> None:
buf = AttachmentBuffer()
_stage(buf, content=b"a", ws="ws1", user="u1")
_stage(buf, content=b"b", ws="ws1", user="u1")
_stage(buf, content=b"c", ws="ws2", user="u1")
assert len(buf.list_for(ws_id="ws1", user_id="u1")) == 2
assert len(buf.list_for(ws_id="ws2", user_id="u1")) == 1
def test_discard_is_scope_checked() -> None:
buf = AttachmentBuffer()
entry = _stage(buf)
wrong_scope = buf.discard(entry.attachment_id, ws_id="ws2", user_id="u1")
assert wrong_scope is False
right_scope = buf.discard(entry.attachment_id, ws_id="ws1", user_id="u1")
assert right_scope is True
assert buf.get(entry.attachment_id, ws_id="ws1", user_id="u1") is None
def test_ttl_eviction_on_access() -> None:
clock = [0.0]
buf = AttachmentBuffer(ttl_seconds=10.0, clock=lambda: clock[0])
_stage(buf, content=b"x")
clock[0] = 11.0 # past the TTL
assert buf.list_for(ws_id="ws1", user_id="u1") == []
def test_size_cap_evicts_oldest_first() -> None:
clock = [0.0]
buf = AttachmentBuffer(max_total_bytes=10, clock=lambda: clock[0])
clock[0] = 1.0
a = _stage(buf, content=b"aaaaa") # 5 bytes
clock[0] = 2.0
b = _stage(buf, content=b"bbbbb") # +5 → 10, at the ceiling
clock[0] = 3.0
c = _stage(buf, content=b"ccccc") # +5 → 15 > 10 → evict oldest (a)
ids = {e.attachment_id for e in buf.list_for(ws_id="ws1", user_id="u1")}
assert a.attachment_id not in ids
assert {b.attachment_id, c.attachment_id} <= ids
def test_cross_scope_identical_bytes_resolve_independently() -> None:
"""Identical bytes staged from two scopes dedupe to one blob but keep
independent references — so neither scope's send drops the other's upload
(the bug: a hash-only key let the second stage overwrite + rescope the
first, and resolve has no committed-store fallback)."""
buf = AttachmentBuffer()
a = _stage(buf, content=b"shared", ws="wsA", user="u1", filename="a.txt")
b = _stage(buf, content=b"shared", ws="wsB", user="u1", filename="b.txt")
assert a.attachment_id == b.attachment_id # same content hash → one blob
# Both scopes resolve their own staged upload — neither was overwritten —
# and each keeps its own per-scope metadata (filename).
ra = buf.get(a.attachment_id, ws_id="wsA", user_id="u1")
rb = buf.get(b.attachment_id, ws_id="wsB", user_id="u1")
assert ra is not None and ra.content == b"shared" and ra.filename == "a.txt"
assert rb is not None and rb.content == b"shared" and rb.filename == "b.txt"
def test_discard_one_scope_keeps_other_and_evicts_on_last() -> None:
"""Discarding one scope's reference (a committing send draining its own
upload) leaves another scope's pending upload of the same bytes intact; the
shared blob is evicted only when the last reference goes."""
buf = AttachmentBuffer()
h = _stage(buf, content=b"dup", ws="wsA", user="u1").attachment_id
_stage(buf, content=b"dup", ws="wsB", user="u1")
discarded_a = buf.discard(h, ws_id="wsA", user_id="u1")
assert discarded_a is True
assert buf.get(h, ws_id="wsA", user_id="u1") is None # wsA's ref gone
assert buf.get(h, ws_id="wsB", user_id="u1") is not None # wsB's survives
discarded_b = buf.discard(h, ws_id="wsB", user_id="u1")
assert discarded_b is True
assert buf.get(h, ws_id="wsB", user_id="u1") is None # last ref → blob evicted
def test_size_cap_counts_deduped_bytes_once() -> None:
"""The size ceiling bounds bytes actually resident: identical bytes staged
from many scopes count once (not once-per-scope as re-keying would), so
dedup-heavy staging isn't falsely evicted."""
buf = AttachmentBuffer(max_total_bytes=8) # fits exactly one 8-byte blob
for ws in ("wsA", "wsB", "wsC"):
_stage(buf, content=b"eightyte", ws=ws, user="u1") # 8 bytes, same blob
handle = hashlib.sha256(b"eightyte").hexdigest()
for ws in ("wsA", "wsB", "wsC"):
assert buf.get(handle, ws_id=ws, user_id="u1") is not None
def test_singleton_getter_is_stable() -> None:
assert get_attachment_buffer() is get_attachment_buffer()