118 lines
4.4 KiB
Python
118 lines
4.4 KiB
Python
"""Regression (#68454): /new, /resume, /branch must not re-append cold-resumed
|
|
transcript rows when flushing before session rotation.
|
|
|
|
Sibling of #68196 / #68205 (preflight compression path). Cold resume loads
|
|
plain dicts without ``_DB_PERSISTED_MARKER``. If the user rotates immediately
|
|
via /new, /resume, or /branch, the old-session flush must pass
|
|
``conversation_history=`` so identity skip treats the loaded prefix as durable.
|
|
|
|
These tests bind the real ``AIAgent._flush_messages_to_session_db`` methods onto
|
|
a lightweight stand-in so construction never hits network model-metadata
|
|
lookups (offline CI / hung OpenRouter).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
from hermes_state import SessionDB
|
|
from run_agent import AIAgent
|
|
|
|
|
|
def _make_flush_agent(db: SessionDB, session_id: str):
|
|
"""Minimal agent shell that owns the real flush implementation."""
|
|
agent = SimpleNamespace(
|
|
_session_db=db,
|
|
_session_db_created=True,
|
|
_persist_disabled=False,
|
|
session_id=session_id,
|
|
_session_persist_lock=None,
|
|
_flushed_db_message_ids=set(),
|
|
_flushed_db_message_session_id=None,
|
|
_last_flushed_db_idx=0,
|
|
_persist_user_message_idx=None,
|
|
_persist_user_message_override=None,
|
|
_persist_user_message_timestamp=None,
|
|
_pending_cli_user_message=None,
|
|
)
|
|
agent._ensure_db_session = lambda: None
|
|
agent._flush_messages_to_session_db = (
|
|
AIAgent._flush_messages_to_session_db.__get__(agent, AIAgent)
|
|
)
|
|
agent._flush_messages_to_session_db_unlocked = (
|
|
AIAgent._flush_messages_to_session_db_unlocked.__get__(agent, AIAgent)
|
|
)
|
|
return agent
|
|
|
|
|
|
def _contents(rows):
|
|
return [r.get("content") for r in rows]
|
|
|
|
|
|
def test_rotation_flush_without_history_boundary_is_safe(tmp_path: Path) -> None:
|
|
"""Bare flush of cold-resumed rows must NOT double-write (#68454 → #92231).
|
|
|
|
Historically this was a control test asserting the double-write: loaded
|
|
rows carried no ``_DB_PERSISTED_MARKER``, so a flush without
|
|
``conversation_history=`` re-appended the whole transcript. Since #92231
|
|
the loaders stamp the marker at row-materialization time, so even the
|
|
"wrong" call shape (no history boundary) is idempotent.
|
|
"""
|
|
db = SessionDB(db_path=tmp_path / "state.db")
|
|
sid = "COLD_ROTATE_DUP"
|
|
db.create_session(sid, source="cli")
|
|
db.append_message(sid, "user", "persisted question")
|
|
db.append_message(sid, "assistant", "persisted answer")
|
|
|
|
loaded = db.get_messages_as_conversation(sid)
|
|
agent = _make_flush_agent(db, sid)
|
|
agent._flush_messages_to_session_db(loaded) # missing conversation_history=
|
|
|
|
rows = db.get_messages_as_conversation(sid, include_inactive=True)
|
|
assert _contents(rows) == ["persisted question", "persisted answer"]
|
|
|
|
|
|
def test_rotation_flush_with_history_boundary_is_noop(tmp_path: Path) -> None:
|
|
"""Fix shape used by /new, /resume, /branch: pass conversation_history=self list."""
|
|
db = SessionDB(db_path=tmp_path / "state.db")
|
|
sid = "COLD_ROTATE_OK"
|
|
db.create_session(sid, source="cli")
|
|
db.append_message(sid, "user", "persisted question")
|
|
db.append_message(sid, "assistant", "persisted answer")
|
|
|
|
loaded = db.get_messages_as_conversation(sid)
|
|
agent = _make_flush_agent(db, sid)
|
|
agent._flush_messages_to_session_db(
|
|
loaded,
|
|
conversation_history=loaded,
|
|
)
|
|
|
|
rows = db.get_messages_as_conversation(sid, include_inactive=True)
|
|
assert _contents(rows) == ["persisted question", "persisted answer"]
|
|
|
|
|
|
def test_rotation_flush_writes_only_new_tail(tmp_path: Path) -> None:
|
|
"""If a turn added messages after cold resume, only the tail is written."""
|
|
db = SessionDB(db_path=tmp_path / "state.db")
|
|
sid = "COLD_ROTATE_TAIL"
|
|
db.create_session(sid, source="cli")
|
|
db.append_message(sid, "user", "persisted question")
|
|
db.append_message(sid, "assistant", "persisted answer")
|
|
|
|
loaded = db.get_messages_as_conversation(sid)
|
|
live = [*loaded, {"role": "user", "content": "new unpersisted turn"}]
|
|
agent = _make_flush_agent(db, sid)
|
|
agent._flush_messages_to_session_db(
|
|
live,
|
|
conversation_history=loaded,
|
|
)
|
|
|
|
rows = db.get_messages_as_conversation(sid, include_inactive=True)
|
|
assert _contents(rows) == [
|
|
"persisted question",
|
|
"persisted answer",
|
|
"new unpersisted turn",
|
|
]
|
|
|