feat(tui): unify session history and context

This commit is contained in:
Xubin Ren
2026-08-17 20:56:10 +08:00
parent b3c3a82075
commit 6301c0ab57
24 changed files with 880 additions and 84 deletions
+24
View File
@@ -10,6 +10,8 @@ from nanobot.cli.tui_launcher import (
_authenticated_ws_url,
_download_release_tui,
_ensure_gateway,
_initial_tui_chat_id,
_read_tui_chat_id,
_resolve_tui_command,
_websocket_chat_id,
)
@@ -36,6 +38,28 @@ def test_websocket_chat_id(session_id: str, expected: str | None) -> None:
assert _websocket_chat_id(session_id) == expected
def test_tui_chat_state_is_optional_and_validated(tmp_path: Path) -> None:
path = tmp_path / "tui" / "state.json"
assert _read_tui_chat_id(path) is None
path.parent.mkdir()
path.write_text('{"schema_version": 1, "chat_id": "saved-chat"}', encoding="utf-8")
assert _read_tui_chat_id(path) == "saved-chat"
path.write_text('{"chat_id": "bad\\nchat"}', encoding="utf-8")
assert _read_tui_chat_id(path) is None
def test_default_tui_resumes_but_explicit_session_wins(tmp_path: Path) -> None:
path = tmp_path / "tui" / "state.json"
path.parent.mkdir()
path.write_text('{"chat_id": "saved-chat"}', encoding="utf-8")
assert _initial_tui_chat_id(None, path) == "saved-chat"
assert _initial_tui_chat_id("cli:direct", path) == "tui-direct"
assert _initial_tui_chat_id("websocket:chosen", path) == "chosen"
def test_explicit_tui_binary_must_exist(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
+11
View File
@@ -94,6 +94,17 @@ def test_manager_renames_model_preset_in_live_and_persisted_sessions(tmp_path) -
)
def test_read_session_snapshot_does_not_populate_runtime_cache(tmp_path) -> None:
stored = Session(key="websocket:context")
store = MagicMock(spec=SessionStore)
store.load.return_value = stored
manager = SessionManager(tmp_path, store=store)
assert manager.read_session_snapshot(stored.key) is stored
assert manager.get_cached(stored.key) is None
store.load.assert_called_once_with(stored.key)
def test_manager_applies_file_cap_before_store_save(tmp_path) -> None:
store = MagicMock(spec=SessionStore)
archiver = MagicMock()
+56
View File
@@ -0,0 +1,56 @@
from nanobot.session import Session
from nanobot.utils.helpers import estimate_message_tokens
from nanobot.webui.session_context import session_context_payload
def test_session_context_separates_archive_progress_from_replay() -> None:
messages = [
{"role": "user", "content": "old question"},
{"role": "assistant", "content": "old answer"},
{"role": "user", "content": "recent question"},
{"role": "assistant", "content": "recent answer"},
]
session = Session(
key="websocket:context",
messages=messages,
last_consolidated=2,
metadata={
"_last_summary": {
"text": "The archived conversation settled the old question.",
"last_active": "2026-08-13T10:00:00Z",
}
},
)
replay = session.get_history(max_messages=0, include_runtime_context=False)
replay_tokens = sum(estimate_message_tokens(message) for message in replay)
summary_tokens = estimate_message_tokens(
{"role": "system", "content": "The archived conversation settled the old question."}
)
payload = session_context_payload(session)
assert payload == {
"schema_version": 1,
"session_key": "websocket:context",
"total_messages": 4,
"archived_messages": 2,
"replay_messages": len(replay),
"estimated_replay_tokens": replay_tokens,
"estimated_summary_tokens": summary_tokens,
"estimated_session_tokens": replay_tokens + summary_tokens,
"archived_summary": "The archived conversation settled the old question.",
"archived_summary_at": "2026-08-13T10:00:00Z",
}
def test_session_context_tolerates_untrusted_summary_metadata() -> None:
session = Session(
key="websocket:context",
messages=[{"role": "user", "content": "hello"}],
metadata={"_last_summary": "invalid"},
)
payload = session_context_payload(session)
assert payload["archived_summary"] is None
assert payload["archived_summary_at"] is None