mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-08 05:18:49 +03:00
perf(webui): accelerate JSONL session list and thread loading (#5194)
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
"""Tests for shared embedded WebUI HTTP helpers."""
|
||||
|
||||
import gzip
|
||||
import json
|
||||
|
||||
from nanobot.webui.http_utils import http_json_response
|
||||
|
||||
|
||||
def test_http_json_response_compresses_large_payload_when_gzip_is_accepted() -> None:
|
||||
payload = {"message": "响应内容" * 2_000}
|
||||
|
||||
response = http_json_response(payload, accept_encoding="br, gzip; q=0.5")
|
||||
|
||||
assert response.headers["Content-Encoding"] == "gzip"
|
||||
assert response.headers["Vary"] == "Accept-Encoding"
|
||||
assert int(response.headers["Content-Length"]) == len(response.body)
|
||||
assert json.loads(gzip.decompress(response.body)) == payload
|
||||
|
||||
|
||||
def test_http_json_response_preserves_identity_when_gzip_is_rejected() -> None:
|
||||
payload = {"message": "x" * 8_000}
|
||||
|
||||
response = http_json_response(payload, accept_encoding="gzip;q=0, br")
|
||||
|
||||
assert "Content-Encoding" not in response.headers
|
||||
assert response.headers["Vary"] == "Accept-Encoding"
|
||||
assert int(response.headers["Content-Length"]) == len(response.body)
|
||||
assert json.loads(response.body) == payload
|
||||
|
||||
|
||||
def test_http_json_response_does_not_compress_small_payload() -> None:
|
||||
payload = {"ok": True}
|
||||
|
||||
response = http_json_response(payload, accept_encoding="gzip")
|
||||
|
||||
assert "Content-Encoding" not in response.headers
|
||||
assert response.headers["Vary"] == "Accept-Encoding"
|
||||
assert json.loads(response.body) == payload
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -9,6 +10,7 @@ import pytest
|
||||
import nanobot.webui.session_list_index as session_list_index
|
||||
from nanobot.cron.session_turns import CRON_HISTORY_META
|
||||
from nanobot.providers.base import ProviderConversationState
|
||||
from nanobot.security.workspace_access import WORKSPACE_SCOPE_METADATA_KEY
|
||||
from nanobot.session.automation_turns import AUTOMATION_HISTORY_META
|
||||
from nanobot.session.history_visibility import HIDDEN_HISTORY_META
|
||||
from nanobot.session.manager import SessionManager
|
||||
@@ -28,7 +30,7 @@ def test_webui_session_list_reuses_valid_index_without_scanning_files(
|
||||
assert list_webui_sessions(manager)[0]["preview"] == "indexed preview"
|
||||
assert list_webui_sessions(manager)[0]["model_preset"] == "fast"
|
||||
|
||||
def fail_scan(session_manager: SessionManager, path: Path) -> None:
|
||||
def fail_scan(session_manager: SessionManager, path: Path, webui_dir: Path) -> None:
|
||||
raise AssertionError(f"unexpected session file scan: {path}")
|
||||
|
||||
monkeypatch.setattr(session_list_index, "_scan_session_row", fail_scan)
|
||||
@@ -40,6 +42,89 @@ def test_webui_session_list_reuses_valid_index_without_scanning_files(
|
||||
assert rows[0]["model_preset"] == "fast"
|
||||
|
||||
|
||||
def test_webui_session_list_indexes_workspace_scope_and_preserves_null(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
manager = SessionManager(tmp_path)
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
|
||||
scoped = manager.get_or_create("websocket:scoped")
|
||||
scoped.metadata[WORKSPACE_SCOPE_METADATA_KEY] = {
|
||||
"project_path": str(project),
|
||||
"access_mode": "full",
|
||||
"future_extension": "x" * 5000,
|
||||
}
|
||||
manager.save(scoped)
|
||||
explicit_null = manager.get_or_create("websocket:null")
|
||||
explicit_null.metadata[WORKSPACE_SCOPE_METADATA_KEY] = None
|
||||
manager.save(explicit_null)
|
||||
manager.save(manager.get_or_create("websocket:missing"))
|
||||
|
||||
rows = {row["key"]: row for row in list_webui_sessions(manager)}
|
||||
|
||||
assert session_list_index.indexed_workspace_scope(rows["websocket:scoped"]) == (
|
||||
True,
|
||||
{"project_path": str(project), "access_mode": "full"},
|
||||
)
|
||||
assert session_list_index.indexed_workspace_scope(rows["websocket:null"]) == (True, None)
|
||||
assert session_list_index.indexed_workspace_scope(rows["websocket:missing"]) == (False, None)
|
||||
|
||||
scoped.metadata[WORKSPACE_SCOPE_METADATA_KEY]["access_mode"] = "restricted"
|
||||
manager.save(scoped)
|
||||
|
||||
refreshed = {row["key"]: row for row in list_webui_sessions(manager)}
|
||||
assert session_list_index.indexed_workspace_scope(refreshed["websocket:scoped"])[1] == {
|
||||
"project_path": str(project),
|
||||
"access_mode": "restricted",
|
||||
}
|
||||
|
||||
|
||||
def test_webui_session_list_does_not_cache_old_snapshot_with_new_signature(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
manager = SessionManager(tmp_path)
|
||||
session_key = "websocket:scope-race"
|
||||
session = manager.get_or_create(session_key)
|
||||
session.metadata[WORKSPACE_SCOPE_METADATA_KEY] = {
|
||||
"project_path": str(tmp_path),
|
||||
"access_mode": "full",
|
||||
}
|
||||
session.add_message("user", "hello")
|
||||
manager.save(session)
|
||||
session_path = manager._get_session_path(session_key)
|
||||
original_open = open
|
||||
scope_changed = False
|
||||
|
||||
class RacingReader(io.StringIO):
|
||||
def __next__(self) -> str:
|
||||
nonlocal scope_changed
|
||||
if not scope_changed:
|
||||
scope_changed = True
|
||||
current = manager.get_or_create(session_key)
|
||||
current.metadata[WORKSPACE_SCOPE_METADATA_KEY] = {
|
||||
"project_path": str(tmp_path),
|
||||
"access_mode": "restricted",
|
||||
}
|
||||
manager.save(current)
|
||||
return super().__next__()
|
||||
|
||||
def racing_open(path, *args, **kwargs):
|
||||
if Path(path) == session_path:
|
||||
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)[0]
|
||||
second = list_webui_sessions(manager)[0]
|
||||
|
||||
assert session_list_index.indexed_workspace_scope(first)[1]["access_mode"] == "full"
|
||||
assert session_list_index.indexed_workspace_scope(second)[1]["access_mode"] == "restricted"
|
||||
|
||||
|
||||
def test_webui_session_list_rejects_invalid_internal_model_preset_metadata(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -74,9 +159,13 @@ def test_webui_session_list_rescans_only_changed_file(tmp_path: Path, monkeypatc
|
||||
original_scan = session_list_index._scan_session_row
|
||||
scanned: list[str] = []
|
||||
|
||||
def record_scan(session_manager: SessionManager, path: Path) -> dict | None:
|
||||
def record_scan(
|
||||
session_manager: SessionManager,
|
||||
path: Path,
|
||||
webui_dir: Path,
|
||||
) -> dict | None:
|
||||
scanned.append(path.name)
|
||||
return original_scan(session_manager, path)
|
||||
return original_scan(session_manager, path, webui_dir)
|
||||
|
||||
monkeypatch.setattr(session_list_index, "_scan_session_row", record_scan)
|
||||
|
||||
@@ -247,9 +336,13 @@ def test_webui_session_list_rescans_when_transcript_changes(
|
||||
original_scan = session_list_index._scan_session_row
|
||||
scanned: list[str] = []
|
||||
|
||||
def record_scan(session_manager: SessionManager, path: Path) -> dict | None:
|
||||
def record_scan(
|
||||
session_manager: SessionManager,
|
||||
path: Path,
|
||||
webui_dir: Path,
|
||||
) -> dict | None:
|
||||
scanned.append(path.name)
|
||||
return original_scan(session_manager, path)
|
||||
return original_scan(session_manager, path, webui_dir)
|
||||
|
||||
monkeypatch.setattr(session_list_index, "_scan_session_row", record_scan)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user