Files
turnstone/tests/test_attachment_buffer.py
T
Patrick Buckley 0e0d0bbf72 fix: pre-push review fixes from the canonical-trajectory deep-dive
A deep-dive review of the branch surfaced a budget regression, SDK doc
drift, dead code, and stale docstrings. Each was boundary-spiked before
fixing.

- R1 (regression): by-reference document attachments were invisible to the
  token budget — _msg_text_chars returned 0 doc_chars for a
  {type:document,attachment_id} placeholder, and the comment's claim that
  the budget "lands at calibration" was false (calibration discards
  doc_chars). Thread the doc size through _attachments_meta (size_bytes, at
  both the live-append and reconstruct build sites) and count it in
  _msg_text_chars, guarded against double-counting the inline form.
  Regression test added.
- F1: the history-DTO schema description and the TS HistoryEvent docstring
  still advertised the removed reminders/advisories keys and omitted the
  system role; corrected server_schemas.py + sdk/typescript/src/events.ts to
  match the shipped shape. The committed OpenAPI JSON snapshots were already
  ~679 lines stale on main; their regen is left to its own chore branch.
- D1: removed AttachmentBuffer.take() — dead (no production caller; the
  commit path uses discard()) and scope-weak (ws_id only, unlike its
  siblings) — with its test and the now-orphaned Iterable import.
- O1: 4 docstrings referenced the moved ChatSession._fold_system_turns →
  lowering.fold_system_turns.
2026-06-04 11:03:13 -07:00

93 lines
3.1 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)
assert buf.discard(entry.attachment_id, ws_id="ws2", user_id="u1") is False
assert buf.discard(entry.attachment_id, ws_id="ws1", user_id="u1") 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_singleton_getter_is_stable() -> None:
assert get_attachment_buffer() is get_attachment_buffer()