feat: launch first-time setup in webui (#5078)

This commit is contained in:
chengyongru
2026-07-24 21:16:43 +08:00
committed by GitHub
parent e260d9b31c
commit 6e0eb46705
5 changed files with 195 additions and 26 deletions
+88 -6
View File
@@ -25,6 +25,7 @@ from nanobot.cron.webui_metadata import cron_proactive_delivery_metadata
from nanobot.providers.factory import ProviderSnapshot, make_provider, provider_signature
from nanobot.providers.openai_codex_provider import _strip_model_prefix
from nanobot.providers.registry import find_by_name
from nanobot.providers.unconfigured_provider import UnconfiguredProvider
from nanobot.session.webui_turns import WebuiTurnRoutePolicy
from nanobot.webui.metadata import (
WEBUI_MESSAGE_SOURCE_METADATA_KEY,
@@ -2023,6 +2024,7 @@ def test_webui_yes_creates_config_and_enables_local_websocket(
"port": 18888,
"open_browser_url": None,
"webui_bundle_mode": "auto",
"unconfigured_provider_error": None,
}
compact_output = re.sub(r"\s+", " ", _strip_ansi(result.stdout))
assert "bootstrap secret was generated" in compact_output
@@ -2032,19 +2034,92 @@ def test_webui_yes_creates_config_and_enables_local_websocket(
assert "Press Ctrl+C here to stop nanobot" in compact_output
def test_webui_yes_refuses_missing_provider_setup(monkeypatch, tmp_path: Path) -> None:
def test_webui_yes_starts_first_run_without_provider_setup(monkeypatch, tmp_path: Path) -> None:
config_file = tmp_path / "config.json"
seen: dict[str, object] = {}
def _missing_provider(_config: Config, **_kwargs) -> ProviderSnapshot:
raise ValueError("No API key configured for provider 'custom'.")
monkeypatch.setattr(
"nanobot.cli.commands._provider_setup_error",
lambda _config: "No API key configured for provider 'custom'.",
)
_patch_gateway_ports_free(monkeypatch)
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
monkeypatch.setattr(
"nanobot.cli.commands._run_gateway",
lambda config, **kwargs: seen.update(config=config, **kwargs),
)
monkeypatch.setattr("nanobot.providers.factory.build_provider_snapshot", _missing_provider)
result = runner.invoke(app, ["webui", "--config", str(config_file), "--yes", "--no-open"])
assert result.exit_code == 0
assert config_file.exists()
assert seen["unconfigured_provider_error"] == "No API key configured for provider 'custom'."
assert "Configure a provider and model in WebUI Settings → Models." in result.stdout
def test_webui_missing_runtime_env_fails_before_starting_gateway(
monkeypatch,
tmp_path: Path,
) -> None:
config_file = tmp_path / "config.json"
missing_env = "NANOBOT_TEST_MISSING_WEBUI_SECRET"
monkeypatch.delenv(missing_env, raising=False)
config_file.write_text(
json.dumps({
"agents": {
"defaults": {
"provider": "ollama",
"model": "ollama/qwen3",
}
},
"channels": {
"websocket": {
"enabled": True,
"host": "0.0.0.0",
"tokenIssueSecret": f"${{{missing_env}}}",
}
},
}),
encoding="utf-8",
)
monkeypatch.setattr(
"nanobot.cli.commands._run_gateway",
lambda *_args, **_kwargs: pytest.fail("gateway must not start with unresolved config"),
)
result = runner.invoke(app, ["webui", "--config", str(config_file), "--yes", "--no-open"])
assert result.exit_code == 1
assert missing_env in result.stdout
assert f"${{{missing_env}}}" in config_file.read_text(encoding="utf-8")
def test_webui_yes_still_refuses_invalid_custom_model_setup(
monkeypatch,
tmp_path: Path,
) -> None:
config_file = tmp_path / "config.json"
config_file.write_text(
json.dumps({
"agents": {
"defaults": {
"provider": "custom",
"model": "custom/test-model",
}
},
"providers": {
"custom": {
"displayName": "Custom",
}
},
}),
encoding="utf-8",
)
result = runner.invoke(app, ["webui", "--config", str(config_file), "--yes"])
assert result.exit_code == 1
assert "provider/model setup is incomplete" in result.stdout
assert not config_file.exists()
def test_webui_background_starts_runtime_and_opens_browser(monkeypatch, tmp_path: Path) -> None:
@@ -2775,9 +2850,11 @@ def test_gateway_bound_cron_runs_as_session_turn(
assert msg.metadata["thread_id"] == "om_root123"
@pytest.mark.parametrize("setup_error", [None, "No API key configured"])
def test_gateway_local_trigger_queue_submits_agent_turns(
monkeypatch,
tmp_path: Path,
setup_error: str | None,
) -> None:
config = Config()
config.agents.defaults.workspace = str(tmp_path / "config-workspace")
@@ -2885,11 +2962,16 @@ def test_gateway_local_trigger_queue_submits_agent_turns(
_fake_run_local_trigger_queue,
)
cli_commands._run_gateway(config, health_server_enabled=False)
cli_commands._run_gateway(
config,
health_server_enabled=False,
unconfigured_provider_error=setup_error,
)
agent = seen["agent"]
agent_kwargs = seen["agent_from_config_kwargs"]
kwargs = seen["local_trigger_queue_kwargs"]
assert isinstance(agent_kwargs["provider"], UnconfiguredProvider) is bool(setup_error)
assert "local_trigger_store" in agent_kwargs
assert kwargs["store"] is agent_kwargs["local_trigger_store"]
assert "bus" not in kwargs