Files
hermes-webui/tests/test_session_tail_payload.py
T
b5caf83ff9 Release v0.51.279 — Release IU (stage-p3h — preserve Activity/streaming turn on mid-stream scroll #3665) (#3686)
* fix(streaming): preserve Activity + streaming turn when loading earlier messages mid-stream (#3665, #3346)

Co-authored-by: mysoul12138 <839465496@qq.com>

* docs(changelog): v0.51.279 — Release IU (stage-p3h)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: mysoul12138 <839465496@qq.com>
2026-06-05 14:45:31 -07:00

147 lines
5.4 KiB
Python

from types import SimpleNamespace
from unittest.mock import patch
from urllib.parse import urlparse
class _FakeSession:
def __init__(self, messages):
self.session_id = "tail_payload_001"
self.title = "Tail payload"
self.workspace = "/tmp"
self.model = "gpt-test"
self.model_provider = None
self.messages = messages
self.tool_calls = [
{"name": "old-tool", "snippet": "historical snippet", "assistant_msg_idx": 0},
{"name": "visible-tool", "snippet": "visible snippet", "assistant_msg_idx": 1},
]
self.input_tokens = 0
self.output_tokens = 0
self.estimated_cost = 0
self.context_length = 1
self.threshold_tokens = 0
self.last_prompt_tokens = 0
self.active_stream_id = None
self.pending_user_message = None
self.pending_attachments = []
self.pending_started_at = None
self.composer_draft = {}
def compact(self):
return {
"session_id": self.session_id,
"title": self.title,
"workspace": self.workspace,
"model": self.model,
"model_provider": self.model_provider,
"message_count": len(self.messages),
"context_length": self.context_length,
"threshold_tokens": self.threshold_tokens,
"last_prompt_tokens": self.last_prompt_tokens,
"active_stream_id": self.active_stream_id,
"pending_user_message": self.pending_user_message,
"composer_draft": self.composer_draft,
}
def _invoke(session, query=None):
import api.routes as routes
captured = {}
def fake_j(_handler, data, status=200, extra_headers=None):
captured["data"] = data
captured["status"] = status
return data
if query is None:
query = "session_id=tail_payload_001&messages=1&resolve_model=0&msg_limit=1"
parsed = urlparse(f"/api/session?{query}")
with patch("api.routes.get_session", return_value=session), \
patch("api.routes._clear_stale_stream_state", return_value=False), \
patch("api.routes._lookup_cli_session_metadata", return_value={}), \
patch("api.routes.redact_session_data", side_effect=lambda raw: raw), \
patch("api.routes.j", side_effect=fake_j):
routes.handle_get(SimpleNamespace(), parsed)
return captured["data"]["session"]
def test_tail_window_includes_windowed_session_tool_calls_even_when_messages_have_tool_metadata():
session = _FakeSession([
{"role": "user", "content": "older"},
{
"role": "assistant",
"content": "visible",
"tool_calls": [{"id": "call_1", "function": {"name": "tool", "arguments": "{}"}}],
},
])
payload = _invoke(session)
assert payload["messages"] == [session.messages[-1]]
# PR #3665: always return session-level tool_calls (windowed to the
# message window) so the browser can merge them with per-message ones.
assert payload["tool_calls"] == [
{"name": "visible-tool", "snippet": "visible snippet", "assistant_msg_idx": 0}
]
assert payload["_messages_truncated"] is True
def test_tail_window_keeps_only_visible_session_tool_calls_for_legacy_messages_without_metadata():
session = _FakeSession([
{"role": "user", "content": "older"},
{"role": "assistant", "content": "visible legacy message"},
])
payload = _invoke(session)
assert payload["messages"] == [session.messages[-1]]
assert payload["tool_calls"] == [
{"name": "visible-tool", "snippet": "visible snippet", "assistant_msg_idx": 0}
]
assert session.tool_calls[-1]["assistant_msg_idx"] == 1
def test_full_load_keeps_all_session_tool_calls_for_legacy_messages_without_metadata():
session = _FakeSession([
{"role": "user", "content": "older"},
{"role": "assistant", "content": "visible legacy message"},
])
payload = _invoke(
session,
query="session_id=tail_payload_001&messages=1&resolve_model=0",
)
assert payload["messages"] == session.messages
assert payload["tool_calls"] == session.tool_calls
def test_msg_before_window_keeps_only_that_page_session_tool_calls():
session = _FakeSession([
{"role": "user", "content": "first"},
{"role": "assistant", "content": "second legacy message"},
{"role": "assistant", "content": "third legacy message"},
{"role": "assistant", "content": "fourth legacy message"},
])
session.tool_calls = [
{"name": "first-page-tool", "snippet": "kept", "assistant_msg_idx": 1},
{"name": "second-page-tool", "snippet": "also kept", "assistant_msg_idx": 2},
{"name": "tail-tool", "snippet": "not in page", "assistant_msg_idx": 3},
{"name": "unindexed-tool", "snippet": "cannot place"},
]
payload = _invoke(
session,
query="session_id=tail_payload_001&messages=1&resolve_model=0&msg_before=3&msg_limit=2",
)
assert payload["messages"] == session.messages[1:3]
assert payload["tool_calls"] == [
{"name": "first-page-tool", "snippet": "kept", "assistant_msg_idx": 0},
{"name": "second-page-tool", "snippet": "also kept", "assistant_msg_idx": 1},
]
assert session.tool_calls[0]["assistant_msg_idx"] == 1
assert session.tool_calls[1]["assistant_msg_idx"] == 2
assert payload["_messages_offset"] == 1