Files
odysseus/tests/test_replace_messages_multimodal.py
T
falabellamichael d16b849c3e fix(stabilization): harden attachment lifecycle and agent guard signals (#5420)
* fix: harden stabilization attachment and agent guards

* fix(uploads): preserve durable references during cleanup

* fix(uploads): close cleanup and compaction races
2026-07-11 15:14:14 +01:00

118 lines
3.8 KiB
Python

"""replace_messages must persist readable, path-free multimodal history.
Live model input may contain provider-specific media blocks and inline data
URLs. Compaction uses replace_messages for the retained transcript, which must
store readable text plus stable structured attachment references without
copying raw base64 payloads into ChatMessage.content.
"""
import uuid
import pytest
import core.database as cdb
from core.models import ChatMessage
from tests.helpers.sqlite_db import make_temp_sqlite
_TS, _ENGINE, _TMPDB = make_temp_sqlite(cdb.Base.metadata)
@pytest.fixture
def manager(monkeypatch):
import core.session_manager as sm
monkeypatch.setattr(sm, "SessionLocal", _TS)
mgr = sm.SessionManager.__new__(sm.SessionManager)
mgr.sessions = {}
mgr.upload_handler = None
return mgr
def _make_session(sid, owner="alice"):
db = _TS()
try:
db.add(cdb.Session(id=sid, owner=owner, name="chat", model="gpt-4o",
endpoint_url="http://localhost:11434",
archived=False, message_count=1))
db.commit()
finally:
db.close()
def test_multimodal_content_persists_text_and_attachment_ref_without_payload(manager):
sid = "sess-" + uuid.uuid4().hex[:8]
_make_session(sid)
upload_id = "a" * 32 + ".png"
multimodal = [
{"type": "text", "text": "what is this?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA"}},
]
msgs = [ChatMessage(
role="user",
content=multimodal,
metadata={
"attachments": [{
"id": upload_id,
"name": "diagram.png",
"mime": "image/png",
"size": 4,
"checksum_sha256": "sha256-digest",
}]
},
)]
assert manager.replace_messages(sid, msgs) is True
expected = (
"what is this?\n"
"[1 inline media payload omitted]\n"
f"[Attachment: diagram.png | id={upload_id} | mime=image/png | "
"size=4 bytes | sha256=sha256-digest]"
)
db = _TS()
try:
stored = db.query(cdb.ChatMessage).filter_by(session_id=sid).one()
assert stored.content == expected
assert "data:image/png;base64,AAAA" not in stored.content
assert "base64" not in stored.content
assert "AAAA" not in stored.content
finally:
db.close()
# Drop the in-memory cache so the next read hydrates from the DB.
manager.sessions.clear()
reloaded = manager.get_session(sid)
assert len(reloaded.history) == 1
persisted = reloaded.history[0].content
assert isinstance(persisted, str)
assert persisted == expected
assert reloaded.history[0].metadata["attachments"][0]["id"] == upload_id
assert (
reloaded.history[0].metadata["attachments"][0]["checksum_sha256"]
== "sha256-digest"
)
def test_jsonlike_plain_string_content_still_round_trips(manager):
sid = "sess-" + uuid.uuid4().hex[:8]
_make_session(sid)
text = '[{"type": "object", "name": "foo"}]'
msgs = [ChatMessage(role="user", content=text)]
assert manager.replace_messages(sid, msgs) is True
manager.sessions.clear()
reloaded = manager.get_session(sid)
assert isinstance(reloaded.history[0].content, str)
assert reloaded.history[0].content == text
def test_replace_messages_keeps_history_alias_for_context_messages(manager):
sid = "sess-" + uuid.uuid4().hex[:8]
_make_session(sid)
msgs = [ChatMessage(role="user", content="original")]
assert manager.replace_messages(sid, msgs) is True
session = manager.sessions[sid]
assert session.history is session._history
session.history.append(ChatMessage(role="user", content="after direct mutation"))
assert session.get_context_messages()[-1]["content"] == "after direct mutation"