feat(webui): add guided setup flows
* feat(channels): add guided setup flows * test(channels): preserve setup config values * fix(channels): reflect saved setup state * refactor(channels): simplify setup state metadata * fix(channels): harden setup lifecycle * refactor(channels): centralize setup contracts * fix(channels): route setup actions through webui shim * fix(channels): adapt settings for compact screens * fix(models): preserve default preset display * feat(models): add curated Codex catalog * fix(webui): stop attached gateway on interrupt * fix(webui): simplify apps catalog * docs(webui): clarify apps and runtime features * feat(settings): add guided capability setup * fix(webui): harden setup and managed services * test: keep managed runtime checks portable * test: scope POSIX runtime coverage * fix(webui): simplify file settings * feat(files): bundle document reading * fix(webui): harden setup request boundaries * fix(webui): prevent channel setup status squeeze * fix(settings): group provider compatibility aliases * refactor(settings): remove redundant setup surfaces * fix(webui): harden guided setup lifecycle * fix(webui): preserve channel setup compatibility
This commit is contained in:
@@ -12,6 +12,7 @@ from nanobot.config.schema import Config, ModelPresetConfig
|
||||
from nanobot.providers.registry import find_by_name
|
||||
from nanobot.webui.settings_api import (
|
||||
WebUISettingsError,
|
||||
_docs_version,
|
||||
_model_catalog_kind,
|
||||
_oauth_provider_status,
|
||||
create_model_configuration,
|
||||
@@ -20,6 +21,7 @@ from nanobot.webui.settings_api import (
|
||||
settings_payload,
|
||||
settings_usage_payload,
|
||||
update_agent_settings,
|
||||
update_api_settings,
|
||||
update_model_configuration,
|
||||
update_network_safety_settings,
|
||||
update_provider_settings,
|
||||
@@ -31,6 +33,100 @@ DYNAMIC_PROVIDER_NAME = "my-company-api"
|
||||
DYNAMIC_PROVIDER_API_BASE = "https://example.test/v1"
|
||||
|
||||
|
||||
def test_docs_version_uses_released_versions_and_falls_back_for_dev() -> None:
|
||||
assert _docs_version("0.2.3") == "0.2.3"
|
||||
assert _docs_version("0.2.3.post1") == "0.2.3.post1"
|
||||
assert _docs_version("0.2.3.dev0") == "latest"
|
||||
assert _docs_version("0.2.3+editable") == "latest"
|
||||
|
||||
|
||||
def test_settings_payload_includes_versioned_docs(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
save_config(Config(), config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
monkeypatch.setattr("nanobot.webui.settings_api.__version__", "0.2.3")
|
||||
|
||||
payload = settings_payload()
|
||||
|
||||
assert payload["docs"] == {
|
||||
"version": "0.2.3",
|
||||
"base_url": "https://nanobot.wiki/docs/0.2.3",
|
||||
"chat_apps_url": "https://nanobot.wiki/docs/0.2.3/getting-started/chat-apps",
|
||||
"latest_url": "https://nanobot.wiki/docs/latest",
|
||||
}
|
||||
|
||||
|
||||
def test_settings_payload_includes_relocated_capabilities(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config = Config()
|
||||
config.api.port = 9910
|
||||
save_config(config, config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
|
||||
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
|
||||
|
||||
payload = settings_payload()
|
||||
|
||||
assert payload["api"]["port"] == 9910
|
||||
assert payload["api"]["api_key_hint"] is None
|
||||
assert payload["observability"]["provider"] == "langfuse"
|
||||
assert payload["observability"]["configured"] is True
|
||||
|
||||
|
||||
def test_update_api_settings_requires_key_for_network_access(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
save_config(Config(), config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
with pytest.raises(WebUISettingsError, match="API key"):
|
||||
update_api_settings({"host": ["0.0.0.0"], "port": ["8900"]})
|
||||
|
||||
payload = update_api_settings({
|
||||
"host": ["0.0.0.0"],
|
||||
"port": ["9900"],
|
||||
"api_key": ["secret-token"],
|
||||
})
|
||||
saved = load_config(config_path)
|
||||
assert saved.api.host == "0.0.0.0"
|
||||
assert saved.api.port == 9900
|
||||
assert saved.api.api_key == "secret-token"
|
||||
assert payload["api"]["api_key_hint"]
|
||||
|
||||
|
||||
def test_update_api_settings_requires_key_for_specific_network_interface(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
save_config(Config(), config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
with pytest.raises(WebUISettingsError, match="API key"):
|
||||
update_api_settings({"host": ["192.168.1.10"], "port": ["8900"]})
|
||||
|
||||
|
||||
def test_update_api_settings_allows_alternate_loopback_without_key(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
save_config(Config(), config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
update_api_settings({"host": ["127.0.0.2"], "port": ["8900"]})
|
||||
|
||||
assert load_config(config_path).api.host == "127.0.0.2"
|
||||
|
||||
|
||||
def _dynamic_provider_config(
|
||||
*,
|
||||
api_base: str = DYNAMIC_PROVIDER_API_BASE,
|
||||
@@ -348,6 +444,42 @@ def test_settings_payload_includes_dynamic_custom_provider(
|
||||
assert providers[DYNAMIC_PROVIDER_NAME]["api_base"] == DYNAMIC_PROVIDER_API_BASE
|
||||
|
||||
|
||||
def test_settings_payload_groups_opencode_compatibility_alias(tmp_path, monkeypatch) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
save_config(Config(), config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
payload = settings_payload()
|
||||
opencode_rows = [row for row in payload["providers"] if row["label"].startswith("OpenCode")]
|
||||
|
||||
assert [(row["name"], row["label"]) for row in opencode_rows] == [
|
||||
("opencode", "OpenCode Zen"),
|
||||
("opencode_go", "OpenCode Go"),
|
||||
]
|
||||
|
||||
|
||||
def test_settings_payload_keeps_configured_opencode_legacy_alias(tmp_path, monkeypatch) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config = Config.model_validate({
|
||||
"providers": {"opencodeZen": {"apiKey": "legacy-key"}},
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"provider": "opencode_zen",
|
||||
"model": "opencode/deepseek-v4-pro",
|
||||
}
|
||||
},
|
||||
})
|
||||
save_config(config, config_path)
|
||||
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||
|
||||
payload = settings_payload()
|
||||
zen_rows = [row for row in payload["providers"] if row["label"] == "OpenCode Zen"]
|
||||
|
||||
assert len(zen_rows) == 1
|
||||
assert zen_rows[0]["name"] == "opencode_zen"
|
||||
assert zen_rows[0]["configured"] is True
|
||||
|
||||
|
||||
def test_settings_payload_marks_dynamic_custom_provider_without_api_base_unconfigured(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -952,6 +1084,26 @@ def test_provider_models_payload_fetches_openai_compatible_models(
|
||||
assert payload["models"][1]["context_window"] == 65536
|
||||
|
||||
|
||||
def test_provider_models_payload_returns_curated_openai_codex_models() -> None:
|
||||
payload = provider_models_payload({"provider": ["openai_codex"]})
|
||||
|
||||
assert payload["status"] == "available"
|
||||
assert payload["catalog_kind"] == "builtin"
|
||||
assert payload["model_count"] == 7
|
||||
assert payload["models"][0] == {
|
||||
"id": "openai-codex/gpt-5.6-sol",
|
||||
"label": "GPT-5.6-Sol",
|
||||
"description": "Latest frontier agentic coding model.",
|
||||
"owned_by": "OpenAI Codex",
|
||||
"context_window": 372000,
|
||||
}
|
||||
assert [model["id"] for model in payload["models"][:3]] == [
|
||||
"openai-codex/gpt-5.6-sol",
|
||||
"openai-codex/gpt-5.6-terra",
|
||||
"openai-codex/gpt-5.6-luna",
|
||||
]
|
||||
|
||||
|
||||
def test_provider_models_payload_fetches_dynamic_custom_provider_models(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -1044,6 +1196,7 @@ def test_model_catalog_kind_uses_provider_spec_metadata() -> None:
|
||||
assert _model_catalog_kind(find_by_name("skywork")) == "official"
|
||||
assert _model_catalog_kind(find_by_name("anthropic")) == "unsupported"
|
||||
assert _model_catalog_kind(find_by_name("openrouter")) == "catalog"
|
||||
assert _model_catalog_kind(find_by_name("openai_codex")) == "builtin"
|
||||
|
||||
|
||||
def test_create_model_configuration_accepts_configured_oauth_provider(
|
||||
|
||||
Reference in New Issue
Block a user