odysseus/tests/test_fork_session_metadata.py

104 lines
3.4 KiB
Python

"""Forking a session must not mutate the source session's messages.
ChatMessage.metadata is a dict. add_message() -> _persist_message() stamps
_db_id (and timestamp) onto that dict in place. The fork handler used to pass
the source message's metadata dict by reference into the new session, so
persisting the fork rewrote the SOURCE messages' _db_id — breaking
edit/delete-by-id on the original conversation. The fork must copy the dict.
"""
import asyncio
from types import SimpleNamespace
from core.models import ChatMessage
import routes.history_routes as mod
class _FakeSession:
def __init__(self, name="", owner=None):
self.name = name
self.owner = owner
self.endpoint_url = ""
self.model = ""
self.history = []
def add_message(self, message):
# Mirror _persist_message: stamp the in-memory message's metadata.
if message.metadata is None:
message.metadata = {}
message.metadata["_db_id"] = f"new-{len(self.history)}"
self.history.append(message)
class _FakeSessionManager:
def __init__(self, source):
self.sessions = {"src-id": source}
self.created = None
def create_session(self, session_id=None, name=None, endpoint_url=None,
model=None, rag=False, owner=None):
self.created = _FakeSession(name=name, owner=owner)
return self.created
def save_sessions(self):
pass
def _fork_handler(router):
for route in router.routes:
if "/fork" in getattr(route, "path", "") and "POST" in getattr(route, "methods", set()):
return route.endpoint
raise AssertionError("fork route not found")
def test_fork_does_not_corrupt_source_message_metadata(monkeypatch):
import core.database as database
monkeypatch.setattr(mod, "_verify_session_owner", lambda *a, **k: None)
provenance = {
"external_untrusted_context_seen": True,
"workspace_untrusted_context_seen": False,
"odysseus_untrusted_context_seen": False,
"private_data_context_seen": False,
}
merged = []
monkeypatch.setattr(
database,
"get_session_agent_provenance",
lambda session_id: provenance,
)
monkeypatch.setattr(
database,
"merge_session_agent_provenance",
lambda session_id, state: merged.append((session_id, state)) or True,
)
source = _FakeSession(name="Original", owner="alice")
source.history = [
ChatMessage("user", "hi", {"_db_id": "src-0"}),
ChatMessage("assistant", "yo", {"_db_id": "src-1"}),
]
sm = _FakeSessionManager(source)
req = SimpleNamespace()
async def _json():
return {"keep_count": 2}
req.json = _json
router = mod.setup_history_routes(sm)
fork = _fork_handler(router)
result = asyncio.run(fork(request=req, session_id="src-id"))
assert result["status"] == "ok"
assert result["kept"] == 2
# The forked session got its own metadata dicts...
new_session = sm.created
assert new_session.history[0].metadata is not source.history[0].metadata
assert new_session.history[1].metadata is not source.history[1].metadata
# ...and the source session's _db_id values are untouched.
assert source.history[0].metadata["_db_id"] == "src-0"
assert source.history[1].metadata["_db_id"] == "src-1"
assert merged == [(result["id"], provenance)]