Files
nanobot/tests/webui/test_settings_services.py
T

171 lines
6.1 KiB
Python

from __future__ import annotations
import threading
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from nanobot.channels.websocket.runtime import WebSocketConfig
from nanobot.config.loader import load_config, save_config
from nanobot.config.schema import Config
from nanobot.webui.gateway_services import build_gateway_services
from nanobot.webui.settings_api import settings_payload, update_agent_settings, update_api_settings
from nanobot.webui.settings_services import (
WebUIOAuthFlowRegistry,
WebUISettingsServices,
)
class _Flow:
def __init__(self, *, expired: bool = False) -> None:
self.expired = expired
self.cancel_count = 0
def cancel(self) -> None:
self.cancel_count += 1
def _gateway(config_path: Path, workspace: Path):
return build_gateway_services(
config=WebSocketConfig(),
bus=MagicMock(),
session_manager=None,
static_dist_path=None,
workspace_path=workspace,
default_restrict_to_workspace=False,
config_path=config_path,
runtime_model_name=None,
runtime_surface="browser",
runtime_capabilities_overrides=None,
)
def test_gateway_settings_services_isolate_config_paths_and_oauth_flows(
tmp_path: Path,
) -> None:
first_path = tmp_path / "first" / "config.json"
second_path = tmp_path / "second" / "config.json"
first_config = Config()
first_config.api.host = "127.0.0.2"
second_config = Config()
second_config.api.host = "127.0.0.3"
save_config(first_config, first_path)
save_config(second_config, second_path)
first = _gateway(first_path, tmp_path / "first-workspace")
second = _gateway(second_path, tmp_path / "second-workspace")
assert first.settings.config.path == first_path.resolve()
assert second.settings.config.path == second_path.resolve()
assert first.http.settings_routes.settings is first.settings
assert second.http.settings_routes.settings is second.settings
assert first.settings.config.load().api.host == "127.0.0.2"
assert second.settings.config.load().api.host == "127.0.0.3"
assert first.settings.read(settings_payload)["api"]["host"] == "127.0.0.2"
assert second.settings.read(settings_payload)["api"]["host"] == "127.0.0.3"
first.settings.mutate(update_api_settings, {"port": ["19001"]})
assert load_config(first_path).api.port == 19001
assert load_config(second_path).api.port != 19001
first_flow = _Flow()
second_flow = _Flow()
first.settings.oauth_flows.register("openai_codex", "same-id", first_flow)
second.settings.oauth_flows.register("openai_codex", "same-id", second_flow)
assert first.settings.oauth_flows.get("openai_codex", "same-id") is first_flow
assert second.settings.oauth_flows.get("openai_codex", "same-id") is second_flow
first.settings.oauth_flows.clear("openai_codex")
assert first_flow.cancel_count == 1
assert second_flow.cancel_count == 0
def test_settings_mutations_serialize_read_modify_write(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path = tmp_path / "config.json"
save_config(Config(), config_path)
services = WebUISettingsServices.create(config_path)
first_loaded = threading.Event()
release_first = threading.Event()
second_started = threading.Event()
second_loaded = threading.Event()
errors: list[BaseException] = []
from nanobot.webui import settings_api
original_load = settings_api._load_settings_config
def controlled_load(path: Path | None) -> Config:
config = original_load(path)
if threading.current_thread().name == "settings-first":
first_loaded.set()
if not release_first.wait(timeout=2):
raise TimeoutError("timed out waiting to release first settings mutation")
elif threading.current_thread().name == "settings-second":
second_loaded.set()
return config
monkeypatch.setattr(settings_api, "_load_settings_config", controlled_load)
def run_first() -> None:
try:
services.mutate(update_agent_settings, {"timezone": ["Asia/Tokyo"]})
except BaseException as exc: # noqa: BLE001 - re-raised in the test thread
errors.append(exc)
def run_second() -> None:
try:
second_started.set()
services.mutate(update_api_settings, {"host": ["127.0.0.9"]})
except BaseException as exc: # noqa: BLE001 - re-raised in the test thread
errors.append(exc)
first = threading.Thread(target=run_first, name="settings-first")
second = threading.Thread(target=run_second, name="settings-second")
first.start()
assert first_loaded.wait(timeout=2)
second.start()
assert second_started.wait(timeout=2)
assert not second_loaded.wait(timeout=0.1)
release_first.set()
first.join(timeout=2)
second.join(timeout=2)
assert not first.is_alive()
assert not second.is_alive()
assert not errors
saved = load_config(config_path)
assert saved.agents.defaults.timezone == "Asia/Tokyo"
assert saved.api.host == "127.0.0.9"
def test_oauth_registry_preserves_expiry_capacity_completion_and_cancel() -> None:
registry = WebUIOAuthFlowRegistry(max_flows=2)
expired = _Flow(expired=True)
oldest = _Flow()
newest = _Flow()
replacement = _Flow()
registry.register("openai_codex", "expired", expired)
registry.register("openai_codex", "oldest", oldest)
assert expired.cancel_count == 1
assert registry.get("openai_codex", "expired") is None
registry.register("xai_grok", "newest", newest)
registry.register("openai_codex", "replacement", replacement)
assert oldest.cancel_count == 1
assert registry.get("openai_codex", "oldest") is None
assert registry.get("xai_grok", "newest") is newest
assert registry.get("openai_codex", "newest") is None
registry.remove("xai_grok", "newest", newest, cancel=False)
assert newest.cancel_count == 0
assert registry.get("xai_grok", "newest") is None
registry.clear("openai_codex")
assert replacement.cancel_count == 1
assert registry.get("openai_codex", "replacement") is None