fix: make onboarding api-key first
Maintainer edit: collapse the first-run wizard to an API-key-only Quick Start and move lower-frequency provider, model, channel, gateway, and tool settings behind Advanced Settings.
This commit is contained in:
@@ -162,15 +162,8 @@ You will see a menu like this:
|
||||
|
||||
```text
|
||||
> What would you like to configure?
|
||||
[Q] Quick Start (recommended)
|
||||
[P] LLM Provider
|
||||
[M] Model Presets
|
||||
[C] Chat Channel
|
||||
[H] Channel Common
|
||||
[A] Agent Settings
|
||||
[I] API Server
|
||||
[G] Gateway
|
||||
[T] Tools
|
||||
[Q] Quick Start (API key only)
|
||||
[A] Advanced Settings
|
||||
[V] View Configuration Summary
|
||||
[S] Save and Exit
|
||||
[X] Exit Without Saving
|
||||
@@ -185,21 +178,20 @@ Move through the wizard like this:
|
||||
| A field you do not need | Keep the shown default or leave it blank, then press `Enter`. |
|
||||
| A back option | Choose it to return to the previous menu. |
|
||||
|
||||
For the first setup, choose `[Q] Quick Start (recommended)`. It has a recommended path for a local browser UI and an advanced path where you can choose every detail yourself. The other main menu items are advanced settings.
|
||||
For the first setup, choose `[Q] Quick Start (API key only)`. It configures the recommended local browser UI and model choice for you. Use `Advanced Settings` later only if you need a different provider, model, channel, gateway, or tool setup.
|
||||
|
||||
If you are following the OpenRouter example:
|
||||
|
||||
1. Choose `[Q] Quick Start (recommended)`.
|
||||
2. Choose `Recommended: local WebUI`.
|
||||
3. Paste your OpenRouter API key, or press `Enter` and add it to the config file later.
|
||||
4. Review the Quick Start summary.
|
||||
5. Choose `[S] Save and Exit`.
|
||||
1. Choose `[Q] Quick Start (API key only)`.
|
||||
2. Paste your OpenRouter API key, or press `Enter` and add it to the config file later.
|
||||
3. Review the Quick Start summary.
|
||||
4. Choose `[S] Save and Exit`.
|
||||
|
||||
The recommended path enables the local WebUI with default WebSocket settings and uses a built-in model choice. You do not need to choose a model ID for the first run.
|
||||
|
||||
If OpenRouter later says your account cannot use the built-in model choice, return to the wizard, choose `[Q] Quick Start (recommended)`, then choose `Choose provider and entry point` and enter another OpenRouter model ID that your account can access.
|
||||
If OpenRouter later says your account cannot use the built-in model choice, return to the wizard, choose `Advanced Settings`, then edit `Model Presets` and enter another OpenRouter model ID that your account can access.
|
||||
|
||||
If you are using another provider, use the same wizard choices but substitute that provider's values:
|
||||
If you already know that you need another provider, choose `Advanced Settings` instead of Quick Start and enter that provider's values:
|
||||
|
||||
| Wizard field | What to enter |
|
||||
|---|---|
|
||||
|
||||
+63
-219
@@ -54,35 +54,10 @@ _BACK_PRESSED = object() # Sentinel value for back navigation
|
||||
# offer existing presets as choices (e.g. AgentDefaults.model_preset).
|
||||
_MODEL_PRESET_CACHE: set[str] = set()
|
||||
|
||||
_QUICK_START_TARGETS = {
|
||||
"WebUI / local browser (recommended)": "websocket",
|
||||
"No chat channel yet": None,
|
||||
"Telegram": "telegram",
|
||||
"WeChat": "weixin",
|
||||
"WhatsApp": "whatsapp",
|
||||
"Feishu / Lark": "feishu",
|
||||
"Slack": "slack",
|
||||
"Discord": "discord",
|
||||
}
|
||||
_QUICK_START_RECOMMENDED_CHOICE = "Recommended: local WebUI"
|
||||
_QUICK_START_CUSTOM_CHOICE = "Choose provider and entry point"
|
||||
_QUICK_START_RECOMMENDED_PROVIDER = "openrouter"
|
||||
_QUICK_START_RECOMMENDED_MODEL = "anthropic/claude-sonnet-4.5"
|
||||
|
||||
_QUICK_START_CHANNEL_FIELDS = {
|
||||
"telegram": (("token", "Telegram bot token from BotFather"),),
|
||||
"feishu": (
|
||||
("app_id", "Feishu/Lark App ID"),
|
||||
("app_secret", "Feishu/Lark App Secret"),
|
||||
),
|
||||
"slack": (
|
||||
("bot_token", "Slack bot token (xoxb-...)"),
|
||||
("app_token", "Slack app token (xapp-...)"),
|
||||
),
|
||||
"discord": (("token", "Discord bot token"),),
|
||||
}
|
||||
|
||||
_QUICK_START_STEPS = ("Setup", "AI provider", "Entry point", "Review")
|
||||
_QUICK_START_STEPS = ("API key", "WebUI", "Review")
|
||||
|
||||
# Low-contrast terminal palette inspired by JetBrains Darcula/Islands.
|
||||
_UI_ACCENT = "#6B9BFA"
|
||||
@@ -413,9 +388,9 @@ def _show_main_menu_header() -> None:
|
||||
body = Table.grid(expand=True)
|
||||
body.add_column(ratio=1)
|
||||
body.add_row(f"{__logo__} [bold {_UI_TEXT}]nanobot[/] [{_UI_MUTED}]v{__version__}[/]")
|
||||
body.add_row(f"[{_UI_ACCENT}]Quick Start configures one AI model and one entry point.[/]")
|
||||
body.add_row(f"[{_UI_ACCENT}]Quick Start only needs an API key.[/]")
|
||||
body.add_row(
|
||||
f"[{_UI_MUTED}]WebUI, chat channels, and advanced settings stay available when you need them.[/]"
|
||||
f"[{_UI_MUTED}]Provider, channel, model, and gateway settings stay in Advanced.[/]"
|
||||
)
|
||||
console.print(
|
||||
Panel(
|
||||
@@ -1052,17 +1027,6 @@ def _get_provider_names() -> dict[str, str]:
|
||||
return {name: data[0] for name, data in info.items() if name}
|
||||
|
||||
|
||||
def _get_quick_start_provider_choices() -> dict[str, str]:
|
||||
"""Return display-name -> provider key choices for chat-capable providers."""
|
||||
from nanobot.providers.registry import PROVIDERS
|
||||
|
||||
return {
|
||||
spec.display_name or spec.name: spec.name
|
||||
for spec in PROVIDERS
|
||||
if not spec.is_oauth and not spec.is_transcription_only
|
||||
}
|
||||
|
||||
|
||||
def _configure_provider(config: Config, provider_name: str) -> None:
|
||||
"""Configure a single LLM provider."""
|
||||
provider_config = getattr(config.providers, provider_name, None)
|
||||
@@ -1421,14 +1385,6 @@ def _pause() -> None:
|
||||
# --- Quick Start ---
|
||||
|
||||
|
||||
def _quick_start_model_default(config: Config, provider_name: str) -> str:
|
||||
"""Return a low-risk default only when the existing model likely still fits."""
|
||||
current = config.resolve_preset()
|
||||
if provider_name in {"openrouter", "anthropic"}:
|
||||
return current.model
|
||||
return ""
|
||||
|
||||
|
||||
def _set_primary_quick_start_preset(config: Config, provider_name: str, model: str) -> None:
|
||||
"""Store the primary preset used by Quick Start."""
|
||||
config.model_presets["primary"] = ModelPresetConfig(
|
||||
@@ -1454,68 +1410,9 @@ def _show_quick_start_progress(active_step: int) -> None:
|
||||
console.print()
|
||||
|
||||
|
||||
def _configure_quick_start_provider(config: Config) -> bool:
|
||||
"""Configure the minimum needed provider + primary model preset."""
|
||||
_show_quick_start_progress(2)
|
||||
provider_choices = _get_quick_start_provider_choices()
|
||||
answer = _select_with_back(
|
||||
"Choose your AI provider:",
|
||||
list(provider_choices),
|
||||
default="OpenRouter",
|
||||
)
|
||||
if answer is _BACK_PRESSED or answer is None:
|
||||
return False
|
||||
|
||||
assert isinstance(answer, str)
|
||||
provider_name = provider_choices[answer]
|
||||
provider_config = getattr(config.providers, provider_name, None)
|
||||
if provider_config is None:
|
||||
console.print(f"[red]Unknown provider: {provider_name}[/red]")
|
||||
return False
|
||||
|
||||
_display, _is_gateway, is_local, default_api_base = _get_provider_info().get(
|
||||
provider_name, (provider_name, False, False, "")
|
||||
)
|
||||
if default_api_base and not provider_config.api_base:
|
||||
provider_config.api_base = default_api_base
|
||||
|
||||
if not is_local:
|
||||
api_key = _input_with_existing(
|
||||
"API key (leave blank only if this provider does not use one)",
|
||||
provider_config.api_key,
|
||||
"str",
|
||||
)
|
||||
if api_key is not None:
|
||||
provider_config.api_key = api_key or None
|
||||
|
||||
if provider_name == "custom" or is_local:
|
||||
api_base = _input_with_existing(
|
||||
"API base URL",
|
||||
provider_config.api_base,
|
||||
"str",
|
||||
)
|
||||
if api_base is not None:
|
||||
provider_config.api_base = api_base or None
|
||||
|
||||
model = _input_model_with_autocomplete(
|
||||
"Model ID",
|
||||
_quick_start_model_default(config, provider_name),
|
||||
provider_name,
|
||||
)
|
||||
if model is None:
|
||||
return False
|
||||
model = model.strip()
|
||||
if not model:
|
||||
console.print("[yellow]! Model ID is required for Quick Start[/yellow]")
|
||||
return False
|
||||
|
||||
_set_primary_quick_start_preset(config, provider_name, model)
|
||||
return True
|
||||
|
||||
|
||||
def _configure_recommended_provider(config: Config) -> bool:
|
||||
"""Configure the beginner path provider with one API-key prompt."""
|
||||
_show_quick_start_progress(2)
|
||||
_show_quick_start_progress(1)
|
||||
provider_name = _QUICK_START_RECOMMENDED_PROVIDER
|
||||
provider_config = getattr(config.providers, provider_name, None)
|
||||
if provider_config is None:
|
||||
@@ -1546,7 +1443,7 @@ def _configure_recommended_provider(config: Config) -> bool:
|
||||
|
||||
def _enable_quick_start_websocket_defaults(config: Config) -> bool:
|
||||
"""Enable local WebUI with the default WebSocket settings."""
|
||||
_show_quick_start_progress(3)
|
||||
_show_quick_start_progress(2)
|
||||
config_cls = _get_channel_config_class("websocket")
|
||||
if config_cls is None:
|
||||
console.print("[red]No configuration class found for websocket[/red]")
|
||||
@@ -1560,61 +1457,9 @@ def _enable_quick_start_websocket_defaults(config: Config) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _configure_quick_start_channel(config: Config, channel_name: str | None) -> bool:
|
||||
"""Enable one common channel with only the fields needed to connect."""
|
||||
_show_quick_start_progress(3)
|
||||
if channel_name is None:
|
||||
return True
|
||||
|
||||
config_cls = _get_channel_config_class(channel_name)
|
||||
if config_cls is None:
|
||||
console.print(f"[red]No configuration class found for {channel_name}[/red]")
|
||||
return False
|
||||
|
||||
current = getattr(config.channels, channel_name, None) or {}
|
||||
model = config_cls.model_validate(current)
|
||||
if channel_name == "websocket":
|
||||
enable = _input_bool("Enable WebSocket channel for local WebUI", True)
|
||||
if not enable:
|
||||
console.print("[yellow]! WebSocket channel was not enabled[/yellow]")
|
||||
return False
|
||||
if hasattr(model, "enabled"):
|
||||
setattr(model, "enabled", True)
|
||||
updated = _configure_pydantic_model(model, "WebSocket")
|
||||
if updated is None:
|
||||
return False
|
||||
setattr(config.channels, channel_name, updated.model_dump(by_alias=True, exclude_none=True))
|
||||
return True
|
||||
|
||||
channel_cls = _get_channel_class(channel_name)
|
||||
display_name = _get_channel_names().get(channel_name, channel_name)
|
||||
if _channel_supports_login(channel_cls):
|
||||
return _run_channel_login(config, channel_name, model, display_name)
|
||||
|
||||
required_fields = _QUICK_START_CHANNEL_FIELDS.get(channel_name, ())
|
||||
for field_name, prompt in required_fields:
|
||||
value = _input_with_existing(prompt, getattr(model, field_name, ""), "str")
|
||||
if value is not None:
|
||||
setattr(model, field_name, value)
|
||||
|
||||
missing = [
|
||||
prompt
|
||||
for field_name, prompt in required_fields
|
||||
if not str(getattr(model, field_name, "") or "").strip()
|
||||
]
|
||||
if missing:
|
||||
console.print(f"[yellow]! {missing[0]} is required; channel was not enabled[/yellow]")
|
||||
return False
|
||||
|
||||
if hasattr(model, "enabled"):
|
||||
setattr(model, "enabled", True)
|
||||
setattr(config.channels, channel_name, model.model_dump(by_alias=True, exclude_none=True))
|
||||
return True
|
||||
|
||||
|
||||
def _show_quick_start_summary(config: Config, channel_name: str | None) -> None:
|
||||
"""Show the small summary users need before returning to the menu."""
|
||||
_show_quick_start_progress(4)
|
||||
_show_quick_start_progress(3)
|
||||
preset = config.model_presets.get("primary")
|
||||
api_key_status = None
|
||||
if preset:
|
||||
@@ -1655,54 +1500,19 @@ def _show_quick_start_summary(config: Config, channel_name: str | None) -> None:
|
||||
|
||||
|
||||
def _configure_quick_start(config: Config) -> None:
|
||||
"""First-run path: model + optional chat channel, with advanced settings hidden."""
|
||||
"""First-run path: API key + local WebUI, with advanced settings hidden."""
|
||||
console.clear()
|
||||
_show_section_header(
|
||||
"Quick Start",
|
||||
"Start with the local browser UI, or choose each detail yourself.",
|
||||
"Paste one API key. nanobot will use recommended local WebUI defaults.",
|
||||
)
|
||||
_show_quick_start_progress(1)
|
||||
answer = _select_with_back(
|
||||
"Choose setup:",
|
||||
[_QUICK_START_RECOMMENDED_CHOICE, _QUICK_START_CUSTOM_CHOICE, "<- Back"],
|
||||
default=_QUICK_START_RECOMMENDED_CHOICE,
|
||||
)
|
||||
if answer is _BACK_PRESSED or answer is None or answer == "<- Back":
|
||||
return
|
||||
|
||||
assert isinstance(answer, str)
|
||||
|
||||
if answer == _QUICK_START_RECOMMENDED_CHOICE:
|
||||
if not _configure_recommended_provider(config):
|
||||
_pause()
|
||||
return
|
||||
if not _enable_quick_start_websocket_defaults(config):
|
||||
_pause()
|
||||
return
|
||||
_show_quick_start_summary(config, "websocket")
|
||||
if not _configure_recommended_provider(config):
|
||||
_pause()
|
||||
return
|
||||
|
||||
if answer != _QUICK_START_CUSTOM_CHOICE:
|
||||
return
|
||||
|
||||
answer = _select_with_back(
|
||||
"How do you want to use nanobot first?",
|
||||
list(_QUICK_START_TARGETS) + ["<- Back"],
|
||||
default="WebUI / local browser (recommended)",
|
||||
)
|
||||
if answer is _BACK_PRESSED or answer is None or answer == "<- Back":
|
||||
return
|
||||
|
||||
assert isinstance(answer, str)
|
||||
channel_name = _QUICK_START_TARGETS[answer]
|
||||
if not _configure_quick_start_provider(config):
|
||||
if not _enable_quick_start_websocket_defaults(config):
|
||||
_pause()
|
||||
return
|
||||
if not _configure_quick_start_channel(config, channel_name):
|
||||
_pause()
|
||||
return
|
||||
_show_quick_start_summary(config, channel_name)
|
||||
_show_quick_start_summary(config, "websocket")
|
||||
_pause()
|
||||
|
||||
|
||||
@@ -1737,6 +1547,54 @@ def _prompt_main_menu_exit(has_unsaved_changes: bool) -> str:
|
||||
return "resume"
|
||||
|
||||
|
||||
def _configure_advanced_settings(config: Config) -> None:
|
||||
"""Show lower-frequency setup options behind one advanced menu."""
|
||||
last_choice: str | None = None
|
||||
while True:
|
||||
try:
|
||||
console.clear()
|
||||
_show_section_header(
|
||||
"Advanced Settings",
|
||||
"Use these when the default API-key setup is not enough.",
|
||||
)
|
||||
answer = _get_questionary().select(
|
||||
"What would you like to configure?",
|
||||
choices=[
|
||||
"[P] LLM Provider",
|
||||
"[M] Model Presets",
|
||||
"[C] Chat Channel",
|
||||
"[H] Channel Common",
|
||||
"[A] Agent Settings",
|
||||
"[I] API Server",
|
||||
"[G] Gateway",
|
||||
"[T] Tools",
|
||||
"<- Back",
|
||||
],
|
||||
default=last_choice,
|
||||
qmark=">",
|
||||
).ask()
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
if answer is None or answer == "<- Back":
|
||||
break
|
||||
|
||||
_advanced_dispatch = {
|
||||
"[P] LLM Provider": lambda: _configure_providers(config),
|
||||
"[M] Model Presets": lambda: _configure_model_presets(config),
|
||||
"[C] Chat Channel": lambda: _configure_channels(config),
|
||||
"[H] Channel Common": lambda: _configure_general_settings(config, "Channel Common"),
|
||||
"[A] Agent Settings": lambda: _configure_general_settings(config, "Agent Settings"),
|
||||
"[I] API Server": lambda: _configure_general_settings(config, "API Server"),
|
||||
"[G] Gateway": lambda: _configure_general_settings(config, "Gateway"),
|
||||
"[T] Tools": lambda: _configure_general_settings(config, "Tools"),
|
||||
}
|
||||
action_fn = _advanced_dispatch.get(answer)
|
||||
if action_fn:
|
||||
last_choice = answer
|
||||
action_fn()
|
||||
|
||||
|
||||
def run_onboard(initial_config: Config | None = None) -> OnboardResult:
|
||||
"""Run the interactive onboarding questionnaire.
|
||||
|
||||
@@ -1768,15 +1626,8 @@ def run_onboard(initial_config: Config | None = None) -> OnboardResult:
|
||||
answer = _get_questionary().select(
|
||||
"What would you like to configure?",
|
||||
choices=[
|
||||
"[Q] Quick Start (recommended)",
|
||||
"[P] LLM Provider",
|
||||
"[M] Model Presets",
|
||||
"[C] Chat Channel",
|
||||
"[H] Channel Common",
|
||||
"[A] Agent Settings",
|
||||
"[I] API Server",
|
||||
"[G] Gateway",
|
||||
"[T] Tools",
|
||||
"[Q] Quick Start (API key only)",
|
||||
"[A] Advanced Settings",
|
||||
"[V] View Configuration Summary",
|
||||
"[S] Save and Exit",
|
||||
"[X] Exit Without Saving",
|
||||
@@ -1796,15 +1647,8 @@ def run_onboard(initial_config: Config | None = None) -> OnboardResult:
|
||||
continue
|
||||
|
||||
_menu_dispatch = {
|
||||
"[Q] Quick Start (recommended)": lambda: _configure_quick_start(config),
|
||||
"[P] LLM Provider": lambda: _configure_providers(config),
|
||||
"[M] Model Presets": lambda: _configure_model_presets(config),
|
||||
"[C] Chat Channel": lambda: _configure_channels(config),
|
||||
"[H] Channel Common": lambda: _configure_general_settings(config, "Channel Common"),
|
||||
"[A] Agent Settings": lambda: _configure_general_settings(config, "Agent Settings"),
|
||||
"[I] API Server": lambda: _configure_general_settings(config, "API Server"),
|
||||
"[G] Gateway": lambda: _configure_general_settings(config, "Gateway"),
|
||||
"[T] Tools": lambda: _configure_general_settings(config, "Tools"),
|
||||
"[Q] Quick Start (API key only)": lambda: _configure_quick_start(config),
|
||||
"[A] Advanced Settings": lambda: _configure_advanced_settings(config),
|
||||
"[V] View Configuration Summary": lambda: _show_summary(config),
|
||||
}
|
||||
|
||||
|
||||
@@ -417,18 +417,6 @@ class TestProviderChannelInfo:
|
||||
assert isinstance(value, tuple)
|
||||
assert len(value) == 4 # (display_name, needs_api_key, needs_api_base, env_var)
|
||||
|
||||
def test_quick_start_provider_choices_cover_chat_registry(self):
|
||||
from nanobot.cli.onboard import _get_quick_start_provider_choices
|
||||
|
||||
choices = _get_quick_start_provider_choices()
|
||||
|
||||
assert choices["OpenRouter"] == "openrouter"
|
||||
assert choices["Moonshot"] == "moonshot"
|
||||
assert choices["NVIDIA NIM"] == "nvidia"
|
||||
assert "OpenAI Codex" not in choices
|
||||
assert "Github Copilot" not in choices
|
||||
assert "AssemblyAI" not in choices
|
||||
|
||||
|
||||
class _SimpleDraftModel(BaseModel):
|
||||
api_key: str = ""
|
||||
@@ -523,6 +511,7 @@ class TestRunOnboardExitBehavior:
|
||||
|
||||
responses = iter(
|
||||
[
|
||||
"[A] Advanced Settings",
|
||||
"[A] Agent Settings",
|
||||
KeyboardInterrupt(),
|
||||
"[X] Exit Without Saving",
|
||||
@@ -874,7 +863,7 @@ class TestMainMenuUpdate:
|
||||
initial_config = Config()
|
||||
|
||||
responses = iter([
|
||||
"[Q] Quick Start (recommended)",
|
||||
"[Q] Quick Start (API key only)",
|
||||
"[S] Save and Exit",
|
||||
])
|
||||
|
||||
@@ -900,86 +889,6 @@ class TestMainMenuUpdate:
|
||||
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([
|
||||
onboard_wizard._QUICK_START_CUSTOM_CHOICE,
|
||||
"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_custom_webui_opens_websocket_config(self, monkeypatch):
|
||||
"""The custom WebUI path should still expose WebSocket settings."""
|
||||
config = Config()
|
||||
selections = iter([
|
||||
onboard_wizard._QUICK_START_CUSTOM_CHOICE,
|
||||
"WebUI / local browser (recommended)",
|
||||
"OpenRouter",
|
||||
])
|
||||
|
||||
def fake_select_with_back(*_args, **_kwargs):
|
||||
return next(selections)
|
||||
|
||||
def fake_configure(model, display_name):
|
||||
assert display_name == "WebSocket"
|
||||
assert model.enabled is True
|
||||
return model
|
||||
|
||||
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_bool", lambda *a, **kw: True)
|
||||
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fake_configure)
|
||||
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["websocketRequiresToken"] is True
|
||||
assert config.agents.defaults.model_preset == "primary"
|
||||
|
||||
def test_quick_start_recommended_webui_skips_advanced_prompts(self, monkeypatch):
|
||||
"""The beginner path should only ask for the API key and use safe defaults."""
|
||||
config = Config()
|
||||
@@ -992,11 +901,6 @@ class TestMainMenuUpdate:
|
||||
|
||||
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",
|
||||
lambda *_args, **_kwargs: onboard_wizard._QUICK_START_RECOMMENDED_CHOICE,
|
||||
)
|
||||
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "sk-or-test")
|
||||
monkeypatch.setattr(onboard_wizard, "_input_model_with_autocomplete", fail_model_input)
|
||||
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fail_websocket_config)
|
||||
@@ -1037,68 +941,6 @@ class TestMainMenuUpdate:
|
||||
assert rows["API key"] == "add later"
|
||||
assert "add your API key" in rows["Next"]
|
||||
|
||||
def test_quick_start_channel_requires_token_before_enable(self, monkeypatch):
|
||||
"""Quick Start should not enable token-based channels with blank credentials."""
|
||||
config = Config()
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "")
|
||||
|
||||
assert onboard_wizard._configure_quick_start_channel(config, "telegram") is False
|
||||
assert getattr(config.channels, "telegram", None) is None
|
||||
|
||||
def test_quick_start_login_channel_runs_login_without_fields(self, monkeypatch):
|
||||
"""Quick Start should use interactive login for channels that support it."""
|
||||
from nanobot.channels.base import BaseChannel
|
||||
|
||||
config = Config()
|
||||
calls: dict[str, Any] = {}
|
||||
|
||||
class LoginConfig(BaseModel):
|
||||
enabled: bool = False
|
||||
token: str = ""
|
||||
|
||||
class LoginChannel(BaseChannel):
|
||||
name = "loginchat"
|
||||
display_name = "Login Chat"
|
||||
|
||||
async def login(self, force: bool = False) -> bool:
|
||||
calls["force"] = force
|
||||
calls["enabled_during_login"] = self.config.enabled
|
||||
return True
|
||||
|
||||
async def start(self) -> None:
|
||||
pass
|
||||
|
||||
async def stop(self) -> None:
|
||||
pass
|
||||
|
||||
async def send(self, msg) -> None:
|
||||
pass
|
||||
|
||||
def fail_input(*_args, **_kwargs):
|
||||
raise AssertionError("Quick Start should not ask for manual channel fields")
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||
monkeypatch.setattr(
|
||||
onboard_wizard,
|
||||
"_get_channel_config_class",
|
||||
lambda channel: LoginConfig if channel == "loginchat" else None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
onboard_wizard,
|
||||
"_get_channel_class",
|
||||
lambda channel: LoginChannel if channel == "loginchat" else None,
|
||||
)
|
||||
monkeypatch.setattr(onboard_wizard, "_get_channel_names", lambda: {"loginchat": "Login Chat"})
|
||||
monkeypatch.setattr(onboard_wizard, "_input_with_existing", fail_input)
|
||||
|
||||
assert onboard_wizard._configure_quick_start_channel(config, "loginchat") is True
|
||||
|
||||
loginchat = getattr(config.channels, "loginchat")
|
||||
assert loginchat["enabled"] is True
|
||||
assert calls == {"force": False, "enabled_during_login": True}
|
||||
|
||||
def test_configure_login_channel_defaults_to_login(self, monkeypatch):
|
||||
"""The channel wizard should start login before exposing advanced fields."""
|
||||
from nanobot.channels.base import BaseChannel
|
||||
@@ -1154,7 +996,7 @@ class TestMainMenuUpdate:
|
||||
assert calls == {"force": False}
|
||||
|
||||
def test_main_menu_dispatch_includes_channel_common(self):
|
||||
"""Main menu dispatch should route [H] to Channel Common."""
|
||||
"""Advanced menu dispatch should route [H] to Channel Common."""
|
||||
|
||||
# We verify by checking the dispatch table is set up correctly
|
||||
# The menu items are defined inline in run_onboard, so we test
|
||||
@@ -1166,7 +1008,7 @@ class TestMainMenuUpdate:
|
||||
assert "Channel Common" in _SETTINGS_SETTER
|
||||
|
||||
def test_main_menu_dispatch_includes_api_server(self):
|
||||
"""Main menu dispatch should route [I] to API Server."""
|
||||
"""Advanced menu dispatch should route [I] to API Server."""
|
||||
from nanobot.cli.onboard import _SETTINGS_GETTER, _SETTINGS_SECTIONS, _SETTINGS_SETTER
|
||||
|
||||
assert "API Server" in _SETTINGS_SECTIONS
|
||||
@@ -1174,10 +1016,11 @@ class TestMainMenuUpdate:
|
||||
assert "API Server" in _SETTINGS_SETTER
|
||||
|
||||
def test_run_onboard_channel_common_edit(self, monkeypatch):
|
||||
"""run_onboard should handle [H] Channel Common correctly."""
|
||||
"""run_onboard should handle [H] Channel Common through Advanced Settings."""
|
||||
initial_config = Config()
|
||||
|
||||
responses = iter([
|
||||
"[A] Advanced Settings",
|
||||
"[H] Channel Common",
|
||||
KeyboardInterrupt(),
|
||||
"[S] Save and Exit",
|
||||
@@ -1209,10 +1052,11 @@ class TestMainMenuUpdate:
|
||||
assert result.config.channels.send_tool_hints is True
|
||||
|
||||
def test_run_onboard_api_server_edit(self, monkeypatch):
|
||||
"""run_onboard should handle [I] API Server correctly."""
|
||||
"""run_onboard should handle [I] API Server through Advanced Settings."""
|
||||
initial_config = Config()
|
||||
|
||||
responses = iter([
|
||||
"[A] Advanced Settings",
|
||||
"[I] API Server",
|
||||
KeyboardInterrupt(),
|
||||
"[S] Save and Exit",
|
||||
@@ -1552,13 +1396,15 @@ class TestModelPresetWizard:
|
||||
assert callable(_configure_model_presets)
|
||||
|
||||
def test_run_onboard_model_presets_edit(self, monkeypatch):
|
||||
"""run_onboard should handle [M] Model Presets correctly."""
|
||||
"""run_onboard should handle [M] Model Presets through Advanced Settings."""
|
||||
from nanobot.config.schema import ModelPresetConfig
|
||||
|
||||
initial_config = Config()
|
||||
|
||||
responses = iter([
|
||||
"[A] Advanced Settings",
|
||||
"[M] Model Presets",
|
||||
KeyboardInterrupt(),
|
||||
"[S] Save and Exit",
|
||||
])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user