Improve onboard wizard setup flow
This commit is contained in:
@@ -8,7 +8,6 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from nanobot.cli import onboard as onboard_wizard
|
||||
@@ -452,12 +451,23 @@ class TestConfigurePydanticModelDrafts:
|
||||
onboard_wizard, "_input_with_existing", lambda *_args, **_kwargs: text_value
|
||||
)
|
||||
|
||||
def test_discarding_section_keeps_original_model_unchanged(self, monkeypatch):
|
||||
def test_back_commits_section_draft(self, monkeypatch):
|
||||
model = _SimpleDraftModel()
|
||||
self._patch_prompt_helpers(monkeypatch, ["first", "back"])
|
||||
|
||||
result = _configure_pydantic_model(model, "Simple")
|
||||
|
||||
assert result is not None
|
||||
updated = cast(_SimpleDraftModel, result)
|
||||
assert updated.api_key == "secret"
|
||||
assert model.api_key == ""
|
||||
|
||||
def test_cancel_keeps_original_model_unchanged(self, monkeypatch):
|
||||
model = _SimpleDraftModel()
|
||||
self._patch_prompt_helpers(monkeypatch, ["first", None])
|
||||
|
||||
result = _configure_pydantic_model(model, "Simple")
|
||||
|
||||
assert result is None
|
||||
assert model.api_key == ""
|
||||
|
||||
@@ -472,7 +482,7 @@ class TestConfigurePydanticModelDrafts:
|
||||
assert updated.api_key == "secret"
|
||||
assert model.api_key == ""
|
||||
|
||||
def test_nested_section_back_discards_nested_edits(self, monkeypatch):
|
||||
def test_nested_section_back_commits_nested_edits(self, monkeypatch):
|
||||
model = _OuterDraftModel()
|
||||
self._patch_prompt_helpers(monkeypatch, ["first", "first", "back", "done"])
|
||||
|
||||
@@ -480,7 +490,7 @@ class TestConfigurePydanticModelDrafts:
|
||||
|
||||
assert result is not None
|
||||
updated = cast(_OuterDraftModel, result)
|
||||
assert updated.nested.api_key == ""
|
||||
assert updated.nested.api_key == "secret"
|
||||
assert model.nested.api_key == ""
|
||||
|
||||
def test_nested_section_done_commits_nested_edits(self, monkeypatch):
|
||||
@@ -656,8 +666,8 @@ class TestValidateFieldConstraint:
|
||||
|
||||
def test_real_send_max_retries_field(self):
|
||||
"""Validate against the actual ChannelsConfig.send_max_retries field."""
|
||||
from nanobot.config.schema import ChannelsConfig
|
||||
from nanobot.cli.onboard import _validate_field_constraint
|
||||
from nanobot.config.schema import ChannelsConfig
|
||||
|
||||
field_info = ChannelsConfig.model_fields["send_max_retries"]
|
||||
assert _validate_field_constraint(3, field_info) is None
|
||||
@@ -847,14 +857,108 @@ class TestApiServerRegistration:
|
||||
class TestMainMenuUpdate:
|
||||
"""Tests for main menu including new Channel Common and API Server items."""
|
||||
|
||||
def test_run_onboard_quick_start_edit(self, monkeypatch):
|
||||
"""run_onboard should route [Q] to Quick Start."""
|
||||
initial_config = Config()
|
||||
|
||||
responses = iter([
|
||||
"[Q] Quick Start (recommended)",
|
||||
"[S] Save and Exit",
|
||||
])
|
||||
|
||||
class FakePrompt:
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
|
||||
def ask(self):
|
||||
return self.response
|
||||
|
||||
def fake_select(*_args, **_kwargs):
|
||||
return FakePrompt(next(responses))
|
||||
|
||||
def fake_quick_start(config):
|
||||
config.agents.defaults.bot_name = "quickbot"
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
||||
monkeypatch.setattr(onboard_wizard, "questionary", SimpleNamespace(select=fake_select))
|
||||
monkeypatch.setattr(onboard_wizard, "_configure_quick_start", fake_quick_start)
|
||||
|
||||
result = run_onboard(initial_config=initial_config)
|
||||
|
||||
assert result.should_save is True
|
||||
assert result.config.agents.defaults.bot_name == "quickbot"
|
||||
|
||||
def test_quick_start_configures_primary_preset_and_telegram(self, monkeypatch):
|
||||
"""Quick Start should set only the minimum provider, model and channel fields."""
|
||||
config = Config()
|
||||
selections = iter(["Telegram", "OpenRouter"])
|
||||
|
||||
def fake_select_with_back(*_args, **_kwargs):
|
||||
return next(selections)
|
||||
|
||||
def fake_input(prompt, *_args, **_kwargs):
|
||||
if "API key" in prompt:
|
||||
return "sk-or-test"
|
||||
if "Telegram bot token" in prompt:
|
||||
return "123:abc"
|
||||
raise AssertionError(prompt)
|
||||
|
||||
monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
|
||||
monkeypatch.setattr(onboard_wizard, "_input_with_existing", fake_input)
|
||||
monkeypatch.setattr(
|
||||
onboard_wizard,
|
||||
"_input_model_with_autocomplete",
|
||||
lambda *_args, **_kwargs: "anthropic/claude-sonnet-4.5",
|
||||
)
|
||||
monkeypatch.setattr(onboard_wizard, "_print_summary_panel", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_pause", lambda: None)
|
||||
|
||||
onboard_wizard._configure_quick_start(config)
|
||||
|
||||
assert config.providers.openrouter.api_key == "sk-or-test"
|
||||
assert config.providers.openrouter.api_base == "https://openrouter.ai/api/v1"
|
||||
assert config.agents.defaults.model_preset == "primary"
|
||||
assert config.model_presets["primary"].provider == "openrouter"
|
||||
assert config.model_presets["primary"].model == "anthropic/claude-sonnet-4.5"
|
||||
telegram = getattr(config.channels, "telegram")
|
||||
assert telegram["enabled"] is True
|
||||
assert telegram["token"] == "123:abc"
|
||||
|
||||
def test_quick_start_webui_enables_websocket(self, monkeypatch):
|
||||
"""The recommended Quick Start target should create a working WebUI channel."""
|
||||
config = Config()
|
||||
selections = iter(["WebUI only (recommended)", "OpenRouter"])
|
||||
|
||||
def fake_select_with_back(*_args, **_kwargs):
|
||||
return next(selections)
|
||||
|
||||
monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
|
||||
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "sk-or-test")
|
||||
monkeypatch.setattr(
|
||||
onboard_wizard,
|
||||
"_input_model_with_autocomplete",
|
||||
lambda *_args, **_kwargs: "anthropic/claude-sonnet-4.5",
|
||||
)
|
||||
monkeypatch.setattr(onboard_wizard, "_print_summary_panel", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_pause", lambda: None)
|
||||
|
||||
onboard_wizard._configure_quick_start(config)
|
||||
|
||||
websocket = getattr(config.channels, "websocket")
|
||||
assert websocket["enabled"] is True
|
||||
assert websocket["allowFrom"] == ["*"]
|
||||
|
||||
def test_main_menu_dispatch_includes_channel_common(self):
|
||||
"""Main menu dispatch should route [H] to Channel Common."""
|
||||
from nanobot.cli.onboard import run_onboard
|
||||
|
||||
# We verify by checking the dispatch table is set up correctly
|
||||
# The menu items are defined inline in run_onboard, so we test
|
||||
# that _configure_general_settings handles the new sections.
|
||||
from nanobot.cli.onboard import _SETTINGS_SECTIONS, _SETTINGS_GETTER, _SETTINGS_SETTER
|
||||
from nanobot.cli.onboard import _SETTINGS_GETTER, _SETTINGS_SECTIONS, _SETTINGS_SETTER
|
||||
|
||||
assert "Channel Common" in _SETTINGS_SECTIONS
|
||||
assert "Channel Common" in _SETTINGS_GETTER
|
||||
@@ -862,7 +966,7 @@ class TestMainMenuUpdate:
|
||||
|
||||
def test_main_menu_dispatch_includes_api_server(self):
|
||||
"""Main menu dispatch should route [I] to API Server."""
|
||||
from nanobot.cli.onboard import _SETTINGS_SECTIONS, _SETTINGS_GETTER, _SETTINGS_SETTER
|
||||
from nanobot.cli.onboard import _SETTINGS_GETTER, _SETTINGS_SECTIONS, _SETTINGS_SETTER
|
||||
|
||||
assert "API Server" in _SETTINGS_SECTIONS
|
||||
assert "API Server" in _SETTINGS_GETTER
|
||||
@@ -1014,6 +1118,7 @@ class TestIsStrOrNone:
|
||||
|
||||
def test_optional_str_true(self):
|
||||
from typing import Optional
|
||||
|
||||
from nanobot.cli.onboard import _is_str_or_none
|
||||
|
||||
assert _is_str_or_none(Optional[str]) is True
|
||||
@@ -1035,7 +1140,7 @@ class TestConfigurePydanticModelEmptyString:
|
||||
def test_optional_str_empty_string_becomes_none(self, monkeypatch):
|
||||
"""Entering '' for an optional str field should set it to None."""
|
||||
from pydantic import BaseModel
|
||||
from nanobot.cli.onboard import _is_str_or_none
|
||||
|
||||
|
||||
class M(BaseModel):
|
||||
api_key: str | None = None
|
||||
|
||||
Reference in New Issue
Block a user