mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-17 01:26:40 +03:00
feat(webui): add tabbed pane workbench (#5322)
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import nanobot.webui.sidebar_state as sidebar_state
|
||||
from nanobot.webui.sidebar_state import (
|
||||
default_webui_sidebar_state,
|
||||
read_webui_sidebar_state,
|
||||
@@ -17,7 +21,7 @@ def test_sidebar_state_defaults_when_file_missing(tmp_path, monkeypatch) -> None
|
||||
assert webui_sidebar_state_path() == tmp_path / "webui" / "sidebar-state.json"
|
||||
|
||||
|
||||
def test_sidebar_state_normalizes_old_or_partial_payload(tmp_path, monkeypatch) -> None:
|
||||
def test_sidebar_state_normalizes_partial_payload(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
|
||||
path = webui_sidebar_state_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -31,6 +35,23 @@ def test_sidebar_state_normalizes_old_or_partial_payload(tmp_path, monkeypatch)
|
||||
"project_name_overrides": {"/repo": " Core ", "bad": ""},
|
||||
"tags_by_key": {"websocket:a": ["work", "work", ""]},
|
||||
"collapsed_groups": {"Earlier": 1},
|
||||
"workbench": {
|
||||
"version": 1,
|
||||
"tabs": {
|
||||
"tab:websocket:a": {
|
||||
"explicit": True,
|
||||
"title": " Research ",
|
||||
"paneKeys": ["websocket:a", "websocket:b", "websocket:a"],
|
||||
"layoutPaneKeys": ["websocket:b", "missing", "websocket:a"],
|
||||
"layout": "invalid-layout",
|
||||
"splitRatios": [0.4, 2, "bad", float("nan")],
|
||||
},
|
||||
"tab:websocket:b": {
|
||||
"paneKeys": ["websocket:b", "websocket:c"],
|
||||
"layout": "bsp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"view": {"density": "tiny", "show_archived": True, "sort": "nope"},
|
||||
}
|
||||
),
|
||||
@@ -47,6 +68,19 @@ def test_sidebar_state_normalizes_old_or_partial_payload(tmp_path, monkeypatch)
|
||||
assert state["project_name_overrides"] == {"/repo": "Core"}
|
||||
assert state["tags_by_key"] == {"websocket:a": ["work"]}
|
||||
assert state["collapsed_groups"] == {"Earlier": True}
|
||||
assert state["workbench"] == {
|
||||
"version": 1,
|
||||
"tabs": {
|
||||
"tab:websocket:a": {
|
||||
"explicit": True,
|
||||
"title": "Research",
|
||||
"paneKeys": ["websocket:a", "websocket:b"],
|
||||
"layoutPaneKeys": ["websocket:b", "websocket:a"],
|
||||
"layout": "columns",
|
||||
"splitRatios": [0.4, 0.95],
|
||||
},
|
||||
},
|
||||
}
|
||||
assert state["view"] == {
|
||||
"density": "comfortable",
|
||||
"show_previews": False,
|
||||
@@ -80,3 +114,67 @@ def test_sidebar_state_write_is_scoped_to_config_data_dir(tmp_path, monkeypatch)
|
||||
assert state["view"]["sort"] == "manual"
|
||||
assert webui_sidebar_state_path().is_file()
|
||||
assert read_webui_sidebar_state()["pinned_keys"] == ["websocket:a"]
|
||||
|
||||
|
||||
def test_sidebar_state_persists_only_visible_workbench_groups(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
|
||||
tabs = {
|
||||
f"tab:websocket:{index}": {
|
||||
"explicit": False,
|
||||
"paneKeys": [f"websocket:{index}"],
|
||||
"layoutPaneKeys": [f"websocket:{index}"],
|
||||
"layout": "columns",
|
||||
"splitRatios": [],
|
||||
}
|
||||
for index in range(2_000)
|
||||
}
|
||||
|
||||
state = write_webui_sidebar_state({"workbench": {"version": 1, "tabs": tabs}})
|
||||
|
||||
assert state["workbench"] == {"version": 1, "tabs": {}}
|
||||
assert webui_sidebar_state_path().stat().st_size < 2_048
|
||||
|
||||
|
||||
def test_sidebar_state_requires_supported_workbench_version(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
|
||||
|
||||
state = write_webui_sidebar_state(
|
||||
{
|
||||
"workbench": {
|
||||
"version": 2,
|
||||
"tabs": {
|
||||
"tab:websocket:a": {
|
||||
"explicit": True,
|
||||
"paneKeys": ["websocket:a"],
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert state["workbench"] == {"version": 1, "tabs": {}}
|
||||
|
||||
|
||||
def test_sidebar_state_serializes_concurrent_writes(monkeypatch) -> None:
|
||||
counter_lock = threading.Lock()
|
||||
active_writes = 0
|
||||
peak_writes = 0
|
||||
|
||||
def fake_write(raw: dict[str, object]) -> dict[str, object]:
|
||||
nonlocal active_writes, peak_writes
|
||||
with counter_lock:
|
||||
active_writes += 1
|
||||
peak_writes = max(peak_writes, active_writes)
|
||||
time.sleep(0.01)
|
||||
with counter_lock:
|
||||
active_writes -= 1
|
||||
return raw
|
||||
|
||||
monkeypatch.setattr(sidebar_state, "_write_webui_sidebar_state", fake_write)
|
||||
payloads = [{"write": index} for index in range(12)]
|
||||
|
||||
with ThreadPoolExecutor(max_workers=4) as executor:
|
||||
results = list(executor.map(sidebar_state.write_webui_sidebar_state, payloads))
|
||||
|
||||
assert results == payloads
|
||||
assert peak_writes == 1
|
||||
|
||||
Reference in New Issue
Block a user