mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-18 18:16:38 +03:00
fix(webui): restore transcript-only session history
This commit is contained in:
@@ -17,6 +17,13 @@ from nanobot.session.manager import SessionManager
|
||||
from nanobot.session.model_selection import SESSION_MODEL_PRESET_METADATA_KEY
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_webui_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
webui_dir = tmp_path / "webui"
|
||||
webui_dir.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: webui_dir)
|
||||
|
||||
|
||||
def test_webui_session_list_reuses_valid_index_without_scanning_files(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
@@ -208,6 +215,247 @@ def test_webui_session_list_drops_deleted_index_rows(tmp_path: Path) -> None:
|
||||
assert list_webui_sessions(manager) == []
|
||||
|
||||
|
||||
def test_webui_session_list_recovers_transcript_without_canonical_session(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
webui_dir = tmp_path / "webui"
|
||||
webui_dir.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: webui_dir)
|
||||
key = "websocket:restored"
|
||||
transcript = webui_dir / f"{SessionManager.safe_key(key)}.jsonl"
|
||||
transcript.write_text(
|
||||
'{"event":"user","chat_id":"restored","text":"original question",'
|
||||
'"created_at_ms":1785502800000}\n'
|
||||
'{"event":"message","chat_id":"restored","text":"original answer",'
|
||||
'"created_at_ms":1785502801000}\n'
|
||||
'{"event":"turn_end","chat_id":"restored","created_at_ms":1785502802000}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
manager = SessionManager(tmp_path / "workspace")
|
||||
|
||||
[row] = list_webui_sessions(manager)
|
||||
|
||||
assert row["key"] == key
|
||||
assert row["preview"] == "original question"
|
||||
assert row["created_at"] == datetime.fromtimestamp(1785502800).isoformat()
|
||||
assert not manager._get_session_path(key).exists()
|
||||
assert manager.list_sessions() == []
|
||||
|
||||
reloaded = SessionManager(tmp_path / "workspace")
|
||||
assert [row["key"] for row in list_webui_sessions(reloaded)] == [key]
|
||||
|
||||
|
||||
def test_webui_session_list_recovers_colon_chat_id_from_transcript(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
webui_dir = tmp_path / "webui"
|
||||
webui_dir.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: webui_dir)
|
||||
key = "websocket:scope:child"
|
||||
transcript = webui_dir / f"{SessionManager.safe_key(key)}.jsonl"
|
||||
transcript.write_text(
|
||||
'{"event":"user","chat_id":"scope:child","text":"scoped history"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
[row] = list_webui_sessions(SessionManager(tmp_path / "workspace"))
|
||||
|
||||
assert row["key"] == key
|
||||
assert row["preview"] == "scoped history"
|
||||
|
||||
|
||||
def test_webui_session_list_tolerates_invalid_transcript_timestamp(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
key = "websocket:bad-time"
|
||||
transcript = tmp_path / "webui" / f"{SessionManager.safe_key(key)}.jsonl"
|
||||
transcript.write_text(
|
||||
'{"event":"user","chat_id":"bad-time","text":"still visible",'
|
||||
'"created_at_ms":1e100}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
[row] = list_webui_sessions(SessionManager(tmp_path / "workspace"))
|
||||
|
||||
assert row["preview"] == "still visible"
|
||||
datetime.fromisoformat(row["created_at"])
|
||||
|
||||
|
||||
def test_webui_session_list_ignores_invalid_transcript_chat_id(tmp_path: Path) -> None:
|
||||
transcript = tmp_path / "webui" / "websocket_.._outside.jsonl"
|
||||
transcript.write_text(
|
||||
'{"event":"user","chat_id":"../outside","text":"do not expose"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert list_webui_sessions(SessionManager(tmp_path / "workspace")) == []
|
||||
|
||||
|
||||
def test_webui_session_list_recovers_segment_only_transcript(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
webui_dir = tmp_path / "webui"
|
||||
webui_dir.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: webui_dir)
|
||||
key = "websocket:segmented"
|
||||
segments = webui_dir / f"{SessionManager.safe_key(key)}.segments"
|
||||
segments.mkdir()
|
||||
(segments / "000001.jsonl").write_text(
|
||||
'{"event":"user","chat_id":"segmented","text":"older segment"}\n'
|
||||
'{"event":"turn_end","chat_id":"segmented"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
[row] = list_webui_sessions(SessionManager(tmp_path / "workspace"))
|
||||
|
||||
assert row["key"] == key
|
||||
assert row["preview"] == "older segment"
|
||||
|
||||
|
||||
def test_webui_session_list_prefers_canonical_metadata_without_duplicate(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
webui_dir = tmp_path / "webui"
|
||||
webui_dir.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: webui_dir)
|
||||
key = "websocket:canonical"
|
||||
(webui_dir / f"{SessionManager.safe_key(key)}.jsonl").write_text(
|
||||
'{"event":"user","chat_id":"canonical","text":"display copy"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
manager = SessionManager(tmp_path / "workspace")
|
||||
session = manager.get_or_create(key)
|
||||
session.metadata["title"] = "Canonical title"
|
||||
session.add_message("user", "canonical preview")
|
||||
manager.save(session)
|
||||
|
||||
rows = list_webui_sessions(manager)
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["key"] == key
|
||||
assert rows[0]["preview"] == "canonical preview"
|
||||
|
||||
|
||||
def test_webui_session_list_reuses_unchanged_transcript_index(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
webui_dir = tmp_path / "webui"
|
||||
webui_dir.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: webui_dir)
|
||||
key = "websocket:cached-transcript"
|
||||
(webui_dir / f"{SessionManager.safe_key(key)}.jsonl").write_text(
|
||||
'{"event":"user","chat_id":"cached-transcript","text":"cached"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
manager = SessionManager(tmp_path / "workspace")
|
||||
assert list_webui_sessions(manager)[0]["preview"] == "cached"
|
||||
|
||||
def fail_scan(*args, **kwargs):
|
||||
raise AssertionError("unchanged transcript should reuse its index row")
|
||||
|
||||
monkeypatch.setattr(session_list_index, "_scan_transcript_row", fail_scan)
|
||||
|
||||
assert list_webui_sessions(manager)[0]["preview"] == "cached"
|
||||
|
||||
|
||||
def test_webui_session_list_does_not_cache_changed_transcript_with_old_signature(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
webui_dir = tmp_path / "webui"
|
||||
key = "websocket:transcript-race"
|
||||
transcript = webui_dir / f"{SessionManager.safe_key(key)}.jsonl"
|
||||
transcript.write_text(
|
||||
'{"event":"user","chat_id":"transcript-race","text":"initial"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
manager = SessionManager(tmp_path / "workspace")
|
||||
assert list_webui_sessions(manager)[0]["preview"] == "initial"
|
||||
transcript.write_text(
|
||||
'{"event":"user","chat_id":"transcript-race","text":"first scan"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
original_open = open
|
||||
changed = False
|
||||
|
||||
class RacingReader(io.StringIO):
|
||||
def __next__(self) -> str:
|
||||
nonlocal changed
|
||||
if not changed:
|
||||
changed = True
|
||||
transcript.write_text(
|
||||
'{"event":"user","chat_id":"transcript-race","text":"second scan"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
return super().__next__()
|
||||
|
||||
def racing_open(path, *args, **kwargs):
|
||||
if Path(path) == transcript:
|
||||
with original_open(path, *args, **kwargs) as source:
|
||||
return RacingReader(source.read())
|
||||
return original_open(path, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(session_list_index, "open", racing_open, raising=False)
|
||||
|
||||
first = list_webui_sessions(manager)
|
||||
second = list_webui_sessions(manager)
|
||||
|
||||
assert first[0]["preview"] == "first scan"
|
||||
assert second[0]["preview"] == "second scan"
|
||||
|
||||
|
||||
def test_webui_session_list_drops_deleted_transcript_index_row(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
webui_dir = tmp_path / "webui"
|
||||
webui_dir.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: webui_dir)
|
||||
key = "websocket:deleted-transcript"
|
||||
transcript = webui_dir / f"{SessionManager.safe_key(key)}.jsonl"
|
||||
transcript.write_text(
|
||||
'{"event":"user","chat_id":"deleted-transcript","text":"delete me"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
manager = SessionManager(tmp_path / "workspace")
|
||||
assert list_webui_sessions(manager)[0]["key"] == key
|
||||
|
||||
transcript.unlink()
|
||||
|
||||
assert list_webui_sessions(manager) == []
|
||||
|
||||
|
||||
def test_webui_session_list_keeps_runtime_instances_isolated(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
first_dir = tmp_path / "instance-a" / "webui"
|
||||
second_dir = tmp_path / "instance-b" / "webui"
|
||||
first_dir.mkdir(parents=True)
|
||||
second_dir.mkdir(parents=True)
|
||||
(first_dir / "websocket_first.jsonl").write_text(
|
||||
'{"event":"user","chat_id":"first","text":"first instance"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(second_dir / "websocket_second.jsonl").write_text(
|
||||
'{"event":"user","chat_id":"second","text":"second instance"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
manager = SessionManager(tmp_path / "workspace")
|
||||
|
||||
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: first_dir)
|
||||
assert [row["key"] for row in list_webui_sessions(manager)] == ["websocket:first"]
|
||||
|
||||
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: second_dir)
|
||||
assert [row["key"] for row in list_webui_sessions(manager)] == ["websocket:second"]
|
||||
|
||||
|
||||
def test_webui_session_list_ignores_legacy_stem(tmp_path: Path) -> None:
|
||||
manager = SessionManager(tmp_path)
|
||||
legacy_path = manager.sessions_dir / "websocket_legacy.jsonl"
|
||||
@@ -269,7 +517,7 @@ def test_webui_session_list_uses_webui_transcript_activity_for_sort(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
webui_dir = tmp_path / "webui"
|
||||
webui_dir.mkdir()
|
||||
webui_dir.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: webui_dir)
|
||||
|
||||
manager = SessionManager(tmp_path)
|
||||
@@ -311,7 +559,7 @@ def test_webui_session_list_rescans_when_transcript_changes(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
webui_dir = tmp_path / "webui"
|
||||
webui_dir.mkdir()
|
||||
webui_dir.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: webui_dir)
|
||||
|
||||
manager = SessionManager(tmp_path)
|
||||
@@ -416,4 +664,3 @@ def test_session_manager_list_sessions_fallback_time_when_missing(tmp_path: Path
|
||||
assert sessions[0]["updated_at"] is not None
|
||||
datetime.fromisoformat(sessions[0]["created_at"])
|
||||
datetime.fromisoformat(sessions[0]["updated_at"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user