Files
hkuds--nanobot/tests/webui/test_session_list_index.py
T
wehub-resource-sync ba1d0b91a4
Test Suite / webui (push) Failing after 1s
Test Suite / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:06:36 +08:00

270 lines
9.9 KiB
Python

from __future__ import annotations
import os
from datetime import datetime
from pathlib import Path
import nanobot.webui.session_list_index as session_list_index
from nanobot.cron.session_turns import CRON_HISTORY_META
from nanobot.session.automation_turns import AUTOMATION_HISTORY_META
from nanobot.session.history_visibility import HIDDEN_HISTORY_META
from nanobot.session.manager import SessionManager
def test_webui_session_list_reuses_valid_index_without_scanning_files(
tmp_path: Path,
monkeypatch,
) -> None:
manager = SessionManager(tmp_path)
session = manager.get_or_create("websocket:indexed")
session.add_message("user", "indexed preview")
manager.save(session)
assert list_webui_sessions(manager)[0]["preview"] == "indexed preview"
def fail_scan(session_manager: SessionManager, path: Path) -> None:
raise AssertionError(f"unexpected session file scan: {path}")
monkeypatch.setattr(session_list_index, "_scan_session_row", fail_scan)
rows = list_webui_sessions(manager)
assert rows[0]["key"] == "websocket:indexed"
assert rows[0]["preview"] == "indexed preview"
def test_webui_session_list_rescans_only_changed_file(tmp_path: Path, monkeypatch) -> None:
manager = SessionManager(tmp_path)
first = manager.get_or_create("websocket:first")
first.add_message("user", "first")
manager.save(first)
second = manager.get_or_create("websocket:second")
second.add_message("user", "second before")
manager.save(second)
assert {row["preview"] for row in list_webui_sessions(manager)} == {"first", "second before"}
second.messages.clear()
second.add_message("user", "second after")
manager.save(second)
original_scan = session_list_index._scan_session_row
scanned: list[str] = []
def record_scan(session_manager: SessionManager, path: Path) -> dict | None:
scanned.append(path.name)
return original_scan(session_manager, path)
monkeypatch.setattr(session_list_index, "_scan_session_row", record_scan)
rows = list_webui_sessions(manager)
assert scanned == [manager._get_session_path("websocket:second").name]
assert {row["preview"] for row in rows} == {"first", "second after"}
def test_webui_session_list_drops_deleted_index_rows(tmp_path: Path) -> None:
manager = SessionManager(tmp_path)
session = manager.get_or_create("websocket:deleted")
session.add_message("user", "gone")
manager.save(session)
assert list_webui_sessions(manager)[0]["key"] == "websocket:deleted"
assert manager.delete_session("websocket:deleted") is True
assert list_webui_sessions(manager) == []
def test_webui_session_list_skips_cron_internal_user_preview(tmp_path: Path) -> None:
manager = SessionManager(tmp_path)
session = manager.get_or_create("websocket:cron-preview")
session.add_message(
"user",
"Scheduled cron job triggered: 30s-test\n\nInternal reminder prompt",
**{CRON_HISTORY_META: True},
)
session.add_message("assistant", "提醒已经到期。")
manager.save(session)
assert list_webui_sessions(manager)[0]["preview"] == "提醒已经到期。"
def test_webui_session_list_skips_trigger_internal_user_preview(tmp_path: Path) -> None:
manager = SessionManager(tmp_path)
session = manager.get_or_create("websocket:trigger-preview")
session.add_message(
"user",
"Local trigger received: PR review",
**{AUTOMATION_HISTORY_META: {"kind": "local_trigger", "trigger_id": "trg_123"}},
)
session.add_message("assistant", "PR #4502 已经开始 review。")
manager.save(session)
assert list_webui_sessions(manager)[0]["preview"] == "PR #4502 已经开始 review。"
def test_webui_session_list_skips_hidden_history_user_preview(tmp_path: Path) -> None:
manager = SessionManager(tmp_path)
session = manager.get_or_create("websocket:hidden-preview")
session.add_message(
"user",
"internal subagent result",
**{HIDDEN_HISTORY_META: {"kind": "subagent_result", "subagent_task_id": "sub-1"}},
)
session.add_message("assistant", "subagent summary")
manager.save(session)
assert list_webui_sessions(manager)[0]["preview"] == "subagent summary"
def test_webui_session_list_uses_webui_transcript_activity_for_sort(
tmp_path: Path,
monkeypatch,
) -> None:
webui_dir = tmp_path / "webui"
webui_dir.mkdir()
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: webui_dir)
manager = SessionManager(tmp_path)
old_session = manager.get_or_create("websocket:old-metadata")
old_session.created_at = datetime(2026, 6, 15, 10, 0, 0)
old_session.updated_at = datetime(2026, 6, 15, 10, 0, 0)
old_session.add_message("user", "old metadata")
old_session.messages[-1]["timestamp"] = "2026-06-15T10:00:00"
old_session.updated_at = datetime(2026, 6, 15, 10, 0, 0)
manager.save(old_session)
newer_metadata = manager.get_or_create("websocket:newer-metadata")
newer_metadata.created_at = datetime(2026, 6, 15, 11, 0, 0)
newer_metadata.updated_at = datetime(2026, 6, 15, 11, 0, 0)
newer_metadata.add_message("user", "newer metadata")
newer_metadata.messages[-1]["timestamp"] = "2026-06-15T11:00:00"
newer_metadata.updated_at = datetime(2026, 6, 15, 11, 0, 0)
manager.save(newer_metadata)
transcript = webui_dir / "websocket_old-metadata.jsonl"
transcript.write_text(
'{"event":"turn_end","chat_id":"old-metadata"}\n',
encoding="utf-8",
)
activity_ns = int(datetime(2026, 6, 15, 12, 0, 0).timestamp() * 1_000_000_000)
os.utime(transcript, ns=(activity_ns, activity_ns))
rows = list_webui_sessions(manager)
assert [row["key"] for row in rows] == [
"websocket:old-metadata",
"websocket:newer-metadata",
]
assert rows[0]["updated_at"].startswith("2026-06-15T12:00:00")
def test_webui_session_list_rescans_when_transcript_changes(
tmp_path: Path,
monkeypatch,
) -> None:
webui_dir = tmp_path / "webui"
webui_dir.mkdir()
monkeypatch.setattr(session_list_index, "get_webui_dir", lambda: webui_dir)
manager = SessionManager(tmp_path)
session = manager.get_or_create("websocket:transcript-change")
session.created_at = datetime(2026, 6, 15, 10, 0, 0)
session.updated_at = datetime(2026, 6, 15, 10, 0, 0)
session.add_message("user", "preview")
session.messages[-1]["timestamp"] = "2026-06-15T10:00:00"
session.updated_at = datetime(2026, 6, 15, 10, 0, 0)
manager.save(session)
assert list_webui_sessions(manager)[0]["preview"] == "preview"
transcript = webui_dir / "websocket_transcript-change.jsonl"
transcript.write_text(
'{"event":"turn_end","chat_id":"transcript-change"}\n',
encoding="utf-8",
)
activity_ns = int(datetime(2026, 6, 15, 12, 30, 0).timestamp() * 1_000_000_000)
os.utime(transcript, ns=(activity_ns, activity_ns))
original_scan = session_list_index._scan_session_row
scanned: list[str] = []
def record_scan(session_manager: SessionManager, path: Path) -> dict | None:
scanned.append(path.name)
return original_scan(session_manager, path)
monkeypatch.setattr(session_list_index, "_scan_session_row", record_scan)
rows = list_webui_sessions(manager)
assert scanned == [manager._get_session_path("websocket:transcript-change").name]
assert rows[0]["updated_at"].startswith("2026-06-15T12:30:00")
def test_webui_session_list_sorts_by_message_activity_not_maintenance_timestamp(
tmp_path: Path,
) -> None:
manager = SessionManager(tmp_path)
old = manager.get_or_create("websocket:old")
old.created_at = datetime(2026, 6, 1, 10, 0, 0)
old.add_message("user", "old first visible activity")
old.messages[-1]["timestamp"] = "2026-06-01T10:00:00"
old.add_message("assistant", "automation result")
old.messages[-1]["timestamp"] = "2026-06-05T10:00:00"
old.updated_at = datetime(2026, 6, 30, 17, 40, 0)
manager.save(old)
newer = manager.get_or_create("websocket:newer")
newer.created_at = datetime(2026, 6, 4, 10, 0, 0)
newer.add_message("user", "newer real activity")
newer.messages[-1]["timestamp"] = "2026-06-04T10:00:00"
newer.updated_at = datetime(2026, 6, 4, 10, 0, 0)
manager.save(newer)
rows = list_webui_sessions(manager)
assert [row["key"] for row in rows] == ["websocket:old", "websocket:newer"]
assert rows[0]["updated_at"] == "2026-06-05T10:00:00"
def list_webui_sessions(manager: SessionManager) -> list[dict]:
return session_list_index.list_webui_sessions(manager)
def test_webui_session_list_fallback_time_when_missing(tmp_path: Path) -> None:
manager = SessionManager(tmp_path)
path = manager._get_session_path("websocket:missing-time")
path.write_text(
'{"_type": "metadata", "key": "websocket:missing-time"}\n'
'{"_type": "message", "role": "user", "content": "hello"}\n',
encoding="utf-8",
)
rows = list_webui_sessions(manager)
assert len(rows) == 1
assert rows[0]["key"] == "websocket:missing-time"
assert rows[0]["created_at"] is not None
assert rows[0]["updated_at"] is not None
datetime.fromisoformat(rows[0]["created_at"])
datetime.fromisoformat(rows[0]["updated_at"])
def test_session_manager_list_sessions_fallback_time_when_missing(tmp_path: Path) -> None:
manager = SessionManager(tmp_path)
path = manager._get_session_path("websocket:missing-time2")
path.write_text(
'{"_type": "metadata", "key": "websocket:missing-time2"}\n'
'{"_type": "message", "role": "user", "content": "hello"}\n',
encoding="utf-8",
)
sessions = manager.list_sessions()
assert len(sessions) == 1
assert sessions[0]["key"] == "websocket:missing-time2"
assert sessions[0]["created_at"] is not None
assert sessions[0]["updated_at"] is not None
datetime.fromisoformat(sessions[0]["created_at"])
datetime.fromisoformat(sessions[0]["updated_at"])