mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-09-04 10:11:46 +03:00
fix(cli): make gateway persistence explicit
This commit is contained in:
+16
-153
@@ -2214,16 +2214,29 @@ def test_webui_yes_creates_config_and_enables_local_websocket(
|
||||
assert "Last local client exited; the on-demand gateway was stopped" in compact_output
|
||||
|
||||
|
||||
def test_webui_dev_rejects_background_before_creating_config(tmp_path: Path) -> None:
|
||||
def test_webui_background_points_to_the_single_persistent_gateway_command(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
config_file = tmp_path / "config.json"
|
||||
workspace = tmp_path / "workspace"
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["webui", "--dev", "--background", "--yes", "--config", str(config_file)],
|
||||
[
|
||||
"webui",
|
||||
"--background",
|
||||
"--config",
|
||||
str(config_file),
|
||||
"--workspace",
|
||||
str(workspace),
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "--dev cannot be combined with --background" in result.stdout
|
||||
compact_output = _strip_ansi(result.stdout).replace("\n", " ")
|
||||
assert "webui --background` no longer owns gateway lifecycle" in compact_output
|
||||
assert "nanobot gateway --background --config" in compact_output
|
||||
assert "--workspace" in compact_output
|
||||
assert not config_file.exists()
|
||||
|
||||
|
||||
@@ -2471,77 +2484,6 @@ def test_webui_yes_still_refuses_invalid_custom_model_setup(
|
||||
assert config_file.name in result.stdout
|
||||
|
||||
|
||||
def test_webui_background_starts_runtime_and_opens_browser(monkeypatch, tmp_path: Path) -> None:
|
||||
from nanobot.gateway import GatewayStartOptions, GatewayStatus, RuntimeResult
|
||||
|
||||
config_file = tmp_path / "config.json"
|
||||
workspace = tmp_path / "workspace"
|
||||
config_file.write_text("{}")
|
||||
seen: dict[str, object] = {}
|
||||
_patch_webui_provider_ready(monkeypatch)
|
||||
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.webui._prepare_webui_bundle_for_gateway",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
|
||||
class _FakeRuntime:
|
||||
def __init__(self, **kwargs) -> None:
|
||||
seen["runtime_kwargs"] = kwargs
|
||||
self.paths = kwargs["paths"]
|
||||
|
||||
def start_background(self, options: GatewayStartOptions) -> RuntimeResult:
|
||||
seen["start_options"] = options
|
||||
status = GatewayStatus(
|
||||
running=True,
|
||||
pid=123,
|
||||
state_path=tmp_path / "gateway.json",
|
||||
log_path=tmp_path / "gateway.log",
|
||||
port=options.port,
|
||||
reason="running",
|
||||
)
|
||||
return RuntimeResult(True, "gateway_started_background", status)
|
||||
|
||||
monkeypatch.setattr("nanobot.gateway.GatewayRuntime", _FakeRuntime)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.webui._open_webui_browser",
|
||||
lambda url: seen.__setitem__("opened_url", url),
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"webui",
|
||||
"--config",
|
||||
str(config_file),
|
||||
"--workspace",
|
||||
str(workspace),
|
||||
"--background",
|
||||
"--gateway-port",
|
||||
"18889",
|
||||
"--yes",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Gateway started in the background" in result.stdout
|
||||
compact_output = _strip_ansi(result.stdout).replace("\n", " ")
|
||||
assert "nanobot gateway status --config" in compact_output
|
||||
assert "--workspace" in compact_output
|
||||
options = seen["start_options"]
|
||||
assert isinstance(options, GatewayStartOptions)
|
||||
assert options.port == 18889
|
||||
assert options.config_path == str(config_file.resolve(strict=False))
|
||||
assert options.workspace == str(workspace.resolve(strict=False))
|
||||
opened_url = seen["opened_url"]
|
||||
assert isinstance(opened_url, str)
|
||||
assert opened_url.startswith("http://127.0.0.1:8765/#/?bootstrapSecret=")
|
||||
assert "bootstrapSecret=<redacted>" in compact_output
|
||||
assert "bootstrapSecret=" in opened_url
|
||||
assert "Closing the browser does not stop channels or automations" in compact_output
|
||||
assert "nanobot gateway stop --config" in compact_output
|
||||
|
||||
|
||||
def test_open_webui_browser_redacts_bootstrap_secret(monkeypatch, capsys) -> None:
|
||||
opened: list[str] = []
|
||||
url = "http://127.0.0.1:8765/#/?bootstrapSecret=super-secret"
|
||||
@@ -2555,85 +2497,6 @@ def test_open_webui_browser_redacts_bootstrap_secret(monkeypatch, capsys) -> Non
|
||||
assert "super-secret" not in output
|
||||
|
||||
|
||||
def test_webui_background_restarts_when_config_changes_and_gateway_is_running(
|
||||
monkeypatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from nanobot.gateway import GatewayStartOptions, GatewayStatus, RuntimeResult
|
||||
|
||||
config_file = tmp_path / "config.json"
|
||||
workspace = tmp_path / "workspace"
|
||||
config_file.write_text("{}")
|
||||
seen: dict[str, object] = {}
|
||||
_patch_webui_provider_ready(monkeypatch)
|
||||
monkeypatch.setattr("nanobot.cli.webui.sync_workspace_templates", lambda _path: None)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.webui._prepare_webui_bundle_for_gateway",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
|
||||
def _status(options: GatewayStartOptions) -> GatewayStatus:
|
||||
return GatewayStatus(
|
||||
running=True,
|
||||
pid=123,
|
||||
state_path=tmp_path / "gateway.json",
|
||||
log_path=tmp_path / "gateway.log",
|
||||
port=options.port,
|
||||
reason="running",
|
||||
)
|
||||
|
||||
class _FakeRuntime:
|
||||
def __init__(self, **kwargs) -> None:
|
||||
seen["runtime_kwargs"] = kwargs
|
||||
self.paths = kwargs["paths"]
|
||||
|
||||
def start_background(self, options: GatewayStartOptions) -> RuntimeResult:
|
||||
seen["start_options"] = options
|
||||
return RuntimeResult(False, "gateway_already_running", _status(options))
|
||||
|
||||
def restart(self, options: GatewayStartOptions, *, timeout_s: int) -> RuntimeResult:
|
||||
seen["restart_options"] = options
|
||||
seen["restart_timeout"] = timeout_s
|
||||
return RuntimeResult(True, "gateway_started_background", _status(options))
|
||||
|
||||
monkeypatch.setattr("nanobot.gateway.GatewayRuntime", _FakeRuntime)
|
||||
monkeypatch.setattr(
|
||||
"nanobot.cli.webui._open_webui_browser",
|
||||
lambda url: seen.__setitem__("opened_url", url),
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"webui",
|
||||
"--config",
|
||||
str(config_file),
|
||||
"--workspace",
|
||||
str(workspace),
|
||||
"--background",
|
||||
"--gateway-port",
|
||||
"18889",
|
||||
"--yes",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
compact_output = _strip_ansi(result.stdout).replace("\n", " ")
|
||||
assert "WebUI config changed; restarting the background gateway" in compact_output
|
||||
assert "Gateway restarted in the background" in compact_output
|
||||
assert "Gateway is already running" not in compact_output
|
||||
options = seen["restart_options"]
|
||||
assert isinstance(options, GatewayStartOptions)
|
||||
assert options is seen["start_options"]
|
||||
assert seen["restart_timeout"] == 20
|
||||
assert options.port == 18889
|
||||
assert options.config_path == str(config_file.resolve(strict=False))
|
||||
assert options.workspace == str(workspace.resolve(strict=False))
|
||||
opened_url = seen["opened_url"]
|
||||
assert isinstance(opened_url, str)
|
||||
assert opened_url.startswith("http://127.0.0.1:8765/#/?bootstrapSecret=")
|
||||
|
||||
|
||||
def test_webui_foreground_attaches_to_existing_managed_gateway(monkeypatch, tmp_path: Path) -> None:
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text("{}")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -203,6 +204,9 @@ def test_gateway_background_starts_detached_runtime(tmp_path):
|
||||
|
||||
def test_gateway_background_adopts_an_existing_on_demand_gateway(tmp_path):
|
||||
app, fake_runtime, _service, _calls, _prepare_calls = _test_app(tmp_path)
|
||||
lease_state = fake_runtime.paths.state_path.with_name("gateway.clients.json")
|
||||
lease_state.parent.mkdir(parents=True, exist_ok=True)
|
||||
lease_state.write_text('{"auto_stop": true, "clients": {}}', encoding="utf-8")
|
||||
|
||||
def already_running(_options: GatewayStartOptions) -> RuntimeResult:
|
||||
return RuntimeResult(False, "gateway_already_running", fake_runtime.status_value)
|
||||
@@ -212,7 +216,23 @@ def test_gateway_background_adopts_an_existing_on_demand_gateway(tmp_path):
|
||||
result = runner.invoke(app, ["gateway", "--background"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Gateway is already running in the background" in result.stdout
|
||||
assert "promoted to persistent background mode" in result.stdout
|
||||
assert "will keep running after all local clients exit" in result.stdout
|
||||
assert not lease_state.exists()
|
||||
|
||||
|
||||
def test_gateway_background_reports_an_existing_persistent_gateway(tmp_path):
|
||||
app, fake_runtime, _service, _calls, _prepare_calls = _test_app(tmp_path)
|
||||
|
||||
def already_running(_options: GatewayStartOptions) -> RuntimeResult:
|
||||
return RuntimeResult(False, "gateway_already_running", fake_runtime.status_value)
|
||||
|
||||
fake_runtime.start_background = already_running # type: ignore[method-assign]
|
||||
|
||||
result = runner.invoke(app, ["gateway", "--background"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "already running in persistent background mode" in result.stdout
|
||||
|
||||
|
||||
def test_gateway_rejects_conflicting_modes(tmp_path):
|
||||
@@ -267,6 +287,9 @@ def test_gateway_restart_starts_background_runtime(tmp_path):
|
||||
config = Config()
|
||||
config.gateway.port = 18793
|
||||
app, fake_runtime, _service, _calls, prepare_calls = _test_app(tmp_path, config=config)
|
||||
lease_state = fake_runtime.paths.state_path.with_name("gateway.clients.json")
|
||||
lease_state.parent.mkdir(parents=True, exist_ok=True)
|
||||
lease_state.write_text('{"auto_stop": true, "clients": {}}', encoding="utf-8")
|
||||
|
||||
result = runner.invoke(app, ["gateway", "restart", "--timeout", "9", "--verbose"])
|
||||
|
||||
@@ -275,6 +298,25 @@ def test_gateway_restart_starts_background_runtime(tmp_path):
|
||||
assert fake_runtime.stop_timeout == 9
|
||||
assert fake_runtime.restarted_options == GatewayStartOptions(port=18793, verbose=True)
|
||||
assert prepare_calls == [(config, "warn")]
|
||||
assert json.loads(lease_state.read_text(encoding="utf-8"))["auto_stop"] is True
|
||||
|
||||
|
||||
def test_gateway_restart_does_not_create_a_persistent_gateway(tmp_path):
|
||||
app, fake_runtime, _service, _calls, _prepare_calls = _test_app(tmp_path)
|
||||
|
||||
def not_running(
|
||||
_options: GatewayStartOptions, *, timeout_s: int
|
||||
) -> RuntimeResult:
|
||||
fake_runtime.stop_timeout = timeout_s
|
||||
return RuntimeResult(False, "gateway_not_running", fake_runtime.status_value)
|
||||
|
||||
fake_runtime.restart = not_running # type: ignore[method-assign]
|
||||
|
||||
result = runner.invoke(app, ["gateway", "restart"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "there is nothing to restart" in result.stdout
|
||||
assert "nanobot gateway --background" in result.stdout
|
||||
|
||||
|
||||
def test_gateway_install_service_uses_service_installer(tmp_path):
|
||||
|
||||
@@ -187,6 +187,21 @@ def test_concurrent_background_starts_create_only_one_process(tmp_path, monkeypa
|
||||
]
|
||||
|
||||
|
||||
def test_restart_does_not_start_a_gateway_that_is_not_running(tmp_path):
|
||||
spawned: list[list[str]] = []
|
||||
runtime = GatewayRuntime(
|
||||
paths=_paths(tmp_path),
|
||||
platform_name="Linux",
|
||||
popen=lambda command, **_kwargs: spawned.append(command),
|
||||
)
|
||||
|
||||
result = runtime.restart(GatewayStartOptions(port=18790))
|
||||
|
||||
assert result.ok is False
|
||||
assert result.message == "gateway_not_running"
|
||||
assert spawned == []
|
||||
|
||||
|
||||
def test_last_interactive_client_stops_an_on_demand_gateway(tmp_path, monkeypatch):
|
||||
runtime = GatewayRuntime(paths=_paths(tmp_path), platform_name="Linux")
|
||||
stopped: list[int] = []
|
||||
@@ -222,7 +237,7 @@ def test_explicit_background_gateway_survives_the_last_client(tmp_path, monkeypa
|
||||
|
||||
client.acquire()
|
||||
client.mark_ephemeral()
|
||||
GatewayClientLease(runtime, kind="gateway-background").mark_persistent()
|
||||
assert GatewayClientLease(runtime, kind="gateway-background").mark_persistent() is True
|
||||
|
||||
assert client.release() is False
|
||||
assert stopped == []
|
||||
|
||||
Reference in New Issue
Block a user