fix(webui): restore transcript-only session history

This commit is contained in:
Xubin Ren
2026-08-14 03:46:01 +09:00
parent af582246f1
commit fd7eb8e046
4 changed files with 656 additions and 29 deletions
+250 -3
View File
@@ -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"])