test: cover tool-message dedupe and latest paging for include_compacted (#80680)
Dedupe key now includes tool_call_id/tool_calls/tool_name: compaction copies carry those fields verbatim, so identical tool messages across generations still collapse, while distinct tool calls sharing role/content/timestamp are never merged. Add endpoint-level coverage for the desktop's real read path (limit + order=latest + include_compacted=true).
This commit is contained in:
+12
-1
@@ -9219,7 +9219,18 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
|
||||
all_rows = cursor.fetchall()
|
||||
seen: dict = {}
|
||||
for row in all_rows:
|
||||
key = (row["role"], row["content"], row["timestamp"])
|
||||
# Tool fields participate in the dedupe key: compaction copies
|
||||
# them verbatim, so identical tool messages across generations
|
||||
# still collapse, while distinct tool calls that happen to
|
||||
# share role/content/timestamp are never merged.
|
||||
key = (
|
||||
row["role"],
|
||||
row["content"],
|
||||
row["timestamp"],
|
||||
row["tool_call_id"],
|
||||
row["tool_calls"],
|
||||
row["tool_name"],
|
||||
)
|
||||
cur = seen.get(key)
|
||||
if cur is None or (row["active"], row["id"]) > (cur["active"], cur["id"]):
|
||||
seen[key] = row
|
||||
|
||||
@@ -1945,6 +1945,45 @@ class TestWebServerEndpoints:
|
||||
contents = [m["content"] for m in resp.json()["messages"]]
|
||||
assert contents == ["old q", "old a", "summary", "live q", "live a"]
|
||||
|
||||
def test_get_session_messages_latest_page_with_compacted_rows(self):
|
||||
"""The desktop's real read path (getLatestSessionMessages: limit +
|
||||
order=latest + include_compacted=true) pages back from the newest
|
||||
message and returns the window in chronological order.
|
||||
"""
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB()
|
||||
try:
|
||||
db.create_session(session_id="compacted-latest", source="cli")
|
||||
db.append_messages_batch(
|
||||
"compacted-latest",
|
||||
[
|
||||
{"role": "user", "content": "old q"},
|
||||
{"role": "assistant", "content": "old a"},
|
||||
],
|
||||
)
|
||||
db.archive_and_compact(
|
||||
"compacted-latest",
|
||||
[
|
||||
{"role": "assistant", "content": "summary"},
|
||||
{"role": "user", "content": "live q"},
|
||||
{"role": "assistant", "content": "live a"},
|
||||
],
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Display history: old q, old a, summary, live q, live a (5 rows).
|
||||
resp = self.client.get(
|
||||
"/api/sessions/compacted-latest/messages"
|
||||
"?include_compacted=true&limit=2&offset=1&order=latest"
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
contents = [m["content"] for m in resp.json()["messages"]]
|
||||
# Newest-first window of 2, skipping the newest (live a):
|
||||
# summary, live q — chronological order, matching the non-compacted path.
|
||||
assert contents == ["summary", "live q"]
|
||||
|
||||
def test_get_session_messages_omitted_limit_defaults_to_500(self):
|
||||
"""The dashboard must never load an entire unbounded transcript."""
|
||||
from hermes_state import SessionDB
|
||||
|
||||
@@ -205,3 +205,47 @@ class TestDisplayDedupe:
|
||||
page = db.get_messages(sid, include_compacted=True, limit=2, offset=2)
|
||||
assert [m["id"] for m in page] == all_ids[2:]
|
||||
assert len(page) == 2
|
||||
|
||||
def test_distinct_tool_calls_with_same_content_are_not_merged(self, db):
|
||||
"""Two real tool messages that happen to share role/content/timestamp
|
||||
must stay separate: the dedupe key includes the tool fields, so only
|
||||
genuine compaction copies (which copy those fields verbatim) collapse.
|
||||
"""
|
||||
sid = "s1"
|
||||
db.create_session(sid, source="cli")
|
||||
|
||||
def _seed_tool_rows(conn):
|
||||
ts = 1700000000.0
|
||||
for cid in ("call-1", "call-2"):
|
||||
conn.execute(
|
||||
"INSERT INTO messages (session_id, role, content, tool_call_id,"
|
||||
" tool_name, timestamp, active, compacted)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, 1, 0)",
|
||||
(sid, "tool", "identical result", cid, "search", ts),
|
||||
)
|
||||
|
||||
db._execute_write(_seed_tool_rows)
|
||||
msgs = db.get_messages(sid, include_compacted=True)
|
||||
assert len(msgs) == 2
|
||||
assert {m["tool_call_id"] for m in msgs} == {"call-1", "call-2"}
|
||||
|
||||
def test_compaction_copies_of_tool_messages_still_collapse(self, db):
|
||||
"""Tool rows copied by a compaction epoch (identical tool fields) are
|
||||
deduped like any other message, not split by the widened key."""
|
||||
sid = "s1"
|
||||
db.create_session(sid, source="cli")
|
||||
|
||||
def _seed_tool_row(conn):
|
||||
conn.execute(
|
||||
"INSERT INTO messages (session_id, role, content, tool_call_id,"
|
||||
" tool_name, timestamp, active, compacted)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, 1, 0)",
|
||||
(sid, "tool", "result", "call-1", "search", 1700000000.0),
|
||||
)
|
||||
|
||||
db._execute_write(_seed_tool_row)
|
||||
orig = _row_ids(db, sid)
|
||||
self._copy_tail_as_new_generation(db, sid, orig)
|
||||
msgs = db.get_messages(sid, include_compacted=True)
|
||||
assert len(msgs) == 1
|
||||
assert msgs[0]["tool_call_id"] == "call-1"
|
||||
|
||||
Reference in New Issue
Block a user