Files
Octop/tests/unit/users/test_preferences.py
b920ed94dd feat(dashboard): let each account customize the sidebar (#1218)
Store group order, item placement, and hidden entries on the user so navigation can be rearranged without showing items the account cannot access.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-27 18:30:00 +08:00

157 lines
5.5 KiB
Python

"""tests/unit/users/test_preferences.py"""
from __future__ import annotations
import pytest
from octop.infra.errors import ErrorCode, OctopError
from octop.infra.users.preferences import (
ModelReasoningPreference,
get_model_reasoning_from_json,
get_preferred_model_from_json,
get_remote_browser_bookmarks_from_json,
get_sidebar_nav_from_json,
get_timezone_from_preferences_json,
merge_model_preferences_json,
merge_preferences_json,
merge_sidebar_nav_json,
validate_remote_browser_bookmarks,
validate_sidebar_nav,
)
def test_validate_normalizes_and_dedupes_urls() -> None:
items = [
{"url": "example.com", "title": "A"},
{"url": "https://example.com", "title": "B"},
{"url": "https://other.test", "title": "Other"},
]
out = validate_remote_browser_bookmarks(items)
assert len(out) == 2
assert out[0].url == "https://example.com"
assert out[0].title == "A"
assert out[1].url == "https://other.test"
def test_validate_skips_invalid_urls() -> None:
out = validate_remote_browser_bookmarks(
[{"url": "ftp://bad", "title": "x"}, {"url": "", "title": "y"}]
)
assert out == []
def test_validate_title_fallback_to_hostname() -> None:
out = validate_remote_browser_bookmarks([{"url": "https://cloud.tencent.com", "title": ""}])
assert out[0].title == "cloud.tencent.com"
def test_validate_rejects_over_limit() -> None:
items = [{"url": f"https://site{i}.test", "title": f"S{i}"} for i in range(13)]
with pytest.raises(OctopError) as ei:
validate_remote_browser_bookmarks(items)
assert ei.value.code is ErrorCode.SLASH_BAD_ARGS
def test_get_remote_browser_bookmarks_from_json_roundtrip() -> None:
raw = merge_preferences_json(
"{}",
validate_remote_browser_bookmarks(
[{"url": "https://a.test", "title": "A"}],
),
)
out = get_remote_browser_bookmarks_from_json(raw)
assert len(out) == 1
assert out[0].url == "https://a.test"
def test_get_remote_browser_bookmarks_from_json_invalid_payload() -> None:
assert get_remote_browser_bookmarks_from_json("not-json") == []
assert get_remote_browser_bookmarks_from_json('{"remote_browser_bookmarks": "x"}') == []
def test_get_timezone_from_preferences_json() -> None:
assert (
get_timezone_from_preferences_json('{"timezone":"Asia/Shanghai","x":1}') == "Asia/Shanghai"
)
assert get_timezone_from_preferences_json('{"timezone":" "}') is None
assert get_timezone_from_preferences_json('{"x":1}') is None
assert get_timezone_from_preferences_json("not-json") is None
def test_merge_preserves_other_keys() -> None:
merged = merge_preferences_json(
'{"foo": 1}',
validate_remote_browser_bookmarks(
[{"url": "https://a.test", "title": "A"}],
),
)
import json
data = json.loads(merged)
assert data["foo"] == 1
assert len(data["remote_browser_bookmarks"]) == 1
def test_model_preferences_roundtrip_and_preserve_other_keys() -> None:
merged = merge_model_preferences_json(
'{"foo":1}',
preferred_model="token/glm-5",
model_reasoning={"token/glm-5": ModelReasoningPreference(mode="enabled", effort="high")},
)
assert get_preferred_model_from_json(merged) == "token/glm-5"
assert get_model_reasoning_from_json(merged)["token/glm-5"] == ModelReasoningPreference(
mode="enabled",
effort="high",
)
def test_sidebar_nav_roundtrip_preserves_other_keys() -> None:
layout = {
"groups": [
{"id": "settings", "name": "配置"},
{"id": "c_aabbccdd11223344", "name": "常用"},
],
"items": [
{"key": "chat"},
{"key": "personalization", "group": "settings"},
{"key": "tasks", "hidden": True, "group": "settings"},
],
}
merged = merge_sidebar_nav_json('{"foo": 1}', layout)
saved = get_sidebar_nav_from_json(merged)
assert saved is not None
assert saved["groups"][0] == {"id": "settings", "name": "配置"}
assert saved["groups"][1]["id"] == "c_aabbccdd11223344"
hidden = next(item for item in saved["items"] if item["key"] == "tasks")
assert hidden == {"key": "tasks", "hidden": True}
assert '"foo": 1' in merged or '"foo":1' in merged
def test_sidebar_nav_rejects_unknown_item_and_dangling_group() -> None:
with pytest.raises(OctopError) as ei:
validate_sidebar_nav({"groups": [], "items": [{"key": "not-a-nav"}]})
assert ei.value.code is ErrorCode.SLASH_BAD_ARGS
with pytest.raises(OctopError):
validate_sidebar_nav({"groups": [], "items": [{"key": "chat", "group": "settings"}]})
with pytest.raises(OctopError):
validate_sidebar_nav({"groups": [{"id": "c_short", "name": "X"}], "items": []})
def test_sidebar_nav_clear_and_invalid_payload() -> None:
merged = merge_sidebar_nav_json("{}", {"groups": [], "items": [{"key": "chat"}]})
cleared = merge_sidebar_nav_json(merged, None)
assert get_sidebar_nav_from_json(cleared) is None
assert get_sidebar_nav_from_json('{"sidebar_nav": "nope"}') is None
assert get_sidebar_nav_from_json("{}") is None
def test_clear_preferred_model_keeps_reasoning_defaults() -> None:
raw = merge_model_preferences_json(
"{}",
preferred_model="token/glm-5",
model_reasoning={"token/glm-5": ModelReasoningPreference(mode="auto")},
)
cleared = merge_model_preferences_json(raw, preferred_model=None)
assert get_preferred_model_from_json(cleared) is None
assert "token/glm-5" in get_model_reasoning_from_json(cleared)