mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 00:03:01 +03:00
fix(cli): make gateway persistence explicit
This commit is contained in:
@@ -174,10 +174,10 @@ Any normal reply means the provider, model, workspace, and browser gateway are w
|
||||
**Keep nanobot running after you close the terminal**
|
||||
|
||||
```bash
|
||||
nanobot webui --background
|
||||
nanobot gateway --background
|
||||
```
|
||||
|
||||
This starts the same full gateway as `nanobot webui`, opens the browser, and leaves channels and automations running after the launcher exits. Complete first-time model setup with foreground `nanobot webui` before switching to background mode.
|
||||
This is the only command that promotes the shared gateway to persistent background mode. It leaves channels and automations running after every local TUI and WebUI launcher exits. Complete first-time model setup with `nanobot webui` before switching to background mode; open the same localhost WebUI again afterward.
|
||||
|
||||
```bash
|
||||
nanobot gateway status
|
||||
|
||||
@@ -121,7 +121,7 @@ nanobot sessions restore-workspace --config ./bot-a/config.json --workspace ./bo
|
||||
The command never deletes the external store and refuses to overwrite a different existing
|
||||
workspace file. Back up both the config directory and workspace before changing versions.
|
||||
|
||||
Interactive mode uses nanobot's native TypeScript terminal UI. It talks to the same local gateway as the WebUI, so streaming, tool progress, and WebSocket sessions share one protocol instead of maintaining a second agent loop. If no gateway is running, either client starts it on demand. Exiting one TUI or WebUI launcher releases only that client; the last interactive launcher stops the on-demand gateway. `nanobot gateway --background`, `nanobot gateway restart`, and `nanobot webui --background` make it persistent until an explicit `nanobot gateway stop`.
|
||||
Interactive mode uses nanobot's native TypeScript terminal UI. It talks to the same local gateway as the WebUI, so streaming, tool progress, and WebSocket sessions share one protocol instead of maintaining a second agent loop. If no gateway is running, either client starts it on demand. Exiting one TUI or WebUI launcher releases only that client; the last interactive launcher stops the on-demand gateway. Only an explicit `nanobot gateway --background` promotes it to persistent mode. `nanobot gateway restart` preserves the current lifecycle mode, and `nanobot gateway stop` ends either mode.
|
||||
|
||||
The default `--theme auto` mode probes the terminal's real foreground and background colors before first paint and follows supported live appearance changes. Use `--theme light` or `--theme dark` when a terminal or multiplexer does not report its colors reliably. The model preset and workspace access labels above the composer can be clicked to open their selectors; arrow keys, `Enter`, and `Esc` provide the same controls without a mouse. Access changes still pass through the gateway's local-trust and active-turn policy checks.
|
||||
|
||||
@@ -138,7 +138,7 @@ Interactive mode exits with `exit`, `quit`, `/exit`, `/quit`, `:q`, or `Ctrl+D`.
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `nanobot webui` | Create config/workspace if needed, enable the local WebUI channel after confirmation, start the gateway, and open `http://127.0.0.1:8765` |
|
||||
| `nanobot webui --background` | Start or reuse a background gateway, then open the WebUI |
|
||||
| `nanobot webui --background` | Deprecated; prints the equivalent explicit `nanobot gateway --background` command and exits |
|
||||
| `nanobot webui --dev` | Start the gateway and Vite together at `http://127.0.0.1:5173`, with live frontend updates |
|
||||
| `nanobot webui --no-open` | Prepare and start the WebUI without opening a browser |
|
||||
| `nanobot webui --port <port>` | Set the WebUI/WebSocket port |
|
||||
@@ -147,7 +147,9 @@ Interactive mode exits with `exit`, `quit`, `/exit`, `/quit`, `:q`, or `Ctrl+D`.
|
||||
|
||||
First-run WebUI setup binds to `127.0.0.1` by default. Use manual configuration and a WebUI password before exposing the WebSocket channel beyond localhost.
|
||||
|
||||
`--dev` is a foreground source-checkout workflow and cannot be combined with `--background`.
|
||||
`--dev` is a foreground source-checkout workflow. Persistent gateway lifecycle is deliberately
|
||||
owned only by `nanobot gateway --background`; `nanobot webui --background` prints migration
|
||||
guidance instead of silently changing process ownership.
|
||||
It installs frontend dependencies when `webui/node_modules` is missing, proxies to the configured
|
||||
WebSocket channel port, and stops Vite when the launcher exits. The shared on-demand gateway stops
|
||||
only when no other interactive client still holds it.
|
||||
|
||||
@@ -160,4 +160,4 @@ Run:
|
||||
nanobot webui
|
||||
```
|
||||
|
||||
Leave that launcher open while you use nanobot. Pressing `Ctrl+C` disconnects it; the shared gateway stops when it was the last local WebUI or TUI client. Use `nanobot webui --background` only after the normal foreground start and model setup work when you want the gateway to stay online with no clients; then manage it with `nanobot gateway status`, `logs`, `restart`, and `stop`.
|
||||
Leave that launcher open while you use nanobot. Pressing `Ctrl+C` disconnects it; the shared gateway stops when it was the last local WebUI or TUI client. After the normal foreground start and model setup work, use `nanobot gateway --background` when you want the gateway to stay online with no clients; then manage it with `nanobot gateway status`, `logs`, `restart`, and `stop`.
|
||||
|
||||
+5
-5
@@ -25,17 +25,17 @@ it can open before a model is configured so you can finish setup in **Settings
|
||||
→ Models**. The first-run path binds the WebUI to `127.0.0.1` by default, so
|
||||
it is not available from other devices on your LAN.
|
||||
|
||||
Run it in the background when you do not want to keep a terminal open:
|
||||
After model setup, explicitly promote the shared gateway when you do not want to keep a client open:
|
||||
|
||||
```bash
|
||||
nanobot webui --background
|
||||
nanobot gateway --background
|
||||
```
|
||||
|
||||
Complete first-time model setup in a foreground `nanobot webui` session before using
|
||||
`--background`.
|
||||
`nanobot webui --background` is retained only to print migration guidance. This keeps one
|
||||
unambiguous owner for persistent process lifecycle.
|
||||
|
||||
Each foreground WebUI or TUI launcher releases only its own client. The last
|
||||
interactive launcher stops an on-demand gateway. `--background` makes the
|
||||
interactive launcher stops an on-demand gateway. `nanobot gateway --background` makes the
|
||||
gateway persistent; manage it with `nanobot gateway status`, `nanobot gateway
|
||||
logs`, `nanobot gateway restart`, and `nanobot gateway stop`.
|
||||
|
||||
|
||||
+26
-3
@@ -167,14 +167,31 @@ def create_gateway_app(
|
||||
loaded_config=cfg,
|
||||
)
|
||||
)
|
||||
promoted = False
|
||||
if result.ok or result.message == "gateway_already_running":
|
||||
GatewayClientLease(runtime, kind="gateway-background").mark_persistent()
|
||||
promoted = GatewayClientLease(
|
||||
runtime,
|
||||
kind="gateway-background",
|
||||
).mark_persistent()
|
||||
if result.ok:
|
||||
console.print("[green]Gateway started in the background.[/green]")
|
||||
print_status(result.status)
|
||||
return
|
||||
if result.message == "gateway_already_running":
|
||||
console.print("[yellow]Gateway is already running in the background.[/yellow]")
|
||||
if promoted:
|
||||
console.print(
|
||||
"[green]Existing on-demand gateway promoted to persistent "
|
||||
"background mode.[/green]"
|
||||
)
|
||||
console.print(
|
||||
"[dim]It will keep running after all local clients exit; "
|
||||
"use `nanobot gateway stop` to stop it.[/dim]"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
"[yellow]Gateway is already running in persistent "
|
||||
"background mode.[/yellow]"
|
||||
)
|
||||
print_status(result.status)
|
||||
return
|
||||
console.print(f"[yellow]Gateway was not started: {result.message}[/yellow]")
|
||||
@@ -267,10 +284,16 @@ def create_gateway_app(
|
||||
timeout_s=timeout,
|
||||
)
|
||||
if result.ok:
|
||||
GatewayClientLease(runtime, kind="gateway-restart").mark_persistent()
|
||||
console.print("[green]Gateway restarted in the background.[/green]")
|
||||
print_status(result.status)
|
||||
return
|
||||
if result.message == "gateway_not_running":
|
||||
console.print("[yellow]Gateway is not running; there is nothing to restart.[/yellow]")
|
||||
console.print(
|
||||
"[dim]Start a persistent gateway with `nanobot gateway --background`.[/dim]"
|
||||
)
|
||||
print_status(result.status)
|
||||
raise typer.Exit(1)
|
||||
console.print(f"[red]Gateway restart failed: {result.message}[/red]")
|
||||
print_status(result.status)
|
||||
raise typer.Exit(1)
|
||||
|
||||
+18
-18
@@ -82,7 +82,7 @@ def webui(
|
||||
background: bool = typer.Option(
|
||||
False,
|
||||
"--background",
|
||||
help="Keep the gateway running after this command exits",
|
||||
help="Deprecated; use `nanobot gateway --background`",
|
||||
),
|
||||
dev: bool = typer.Option(
|
||||
False,
|
||||
@@ -107,10 +107,24 @@ def webui(
|
||||
)
|
||||
|
||||
cli_terminal._ensure_interactive_tty_mode()
|
||||
if dev and background:
|
||||
console.print("[red]Error: --dev cannot be combined with --background.[/red]")
|
||||
raise typer.Exit(1)
|
||||
config_path = _resolve_webui_config_path(config)
|
||||
if background:
|
||||
import shlex
|
||||
|
||||
command = ["nanobot", "gateway", "--background", "--config", str(config_path)]
|
||||
if workspace:
|
||||
command.extend(
|
||||
["--workspace", str(Path(workspace).expanduser().resolve(strict=False))]
|
||||
)
|
||||
console.print(
|
||||
"[red]`nanobot webui --background` no longer owns gateway lifecycle.[/red]"
|
||||
)
|
||||
console.print("Start the persistent gateway explicitly, then open the WebUI:")
|
||||
console.print(" [cyan]" + " ".join(shlex.quote(part) for part in command) + "[/cyan]")
|
||||
console.print(
|
||||
" [cyan]nanobot webui --config " + shlex.quote(str(config_path)) + "[/cyan]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
created_config = not config_path.exists()
|
||||
if created_config:
|
||||
console.print(f"[yellow]No config found at {config_path}.[/yellow]")
|
||||
@@ -134,12 +148,6 @@ def webui(
|
||||
if settings_setup_error:
|
||||
console.print(f"[yellow]Model setup is incomplete: {provider_error}[/yellow]")
|
||||
console.print("Configure a provider and model in WebUI Settings → Models.")
|
||||
if background:
|
||||
console.print(
|
||||
"[red]First-time WebUI setup must run in the foreground. "
|
||||
"Run `nanobot webui` without --background.[/red]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
elif provider_error:
|
||||
console.print(f"[dim]Provider check: {provider_error}[/dim]")
|
||||
setup_config = _run_quick_start_for_webui(
|
||||
@@ -270,14 +278,6 @@ def webui(
|
||||
f"[cyan]{_gateway_instance_command('stop', config_path=config_path, workspace=workspace)}[/cyan]"
|
||||
)
|
||||
|
||||
if background:
|
||||
ensure_shared_gateway()
|
||||
GatewayClientLease(runtime, kind="webui-background").mark_persistent()
|
||||
print_shared_gateway_controls()
|
||||
if not no_open:
|
||||
_open_webui_browser(webui_url)
|
||||
return
|
||||
|
||||
gateway_ready = _gateway_health_ready(runtime_config.gateway.host, effective_gateway_port)
|
||||
webui_ready = _webui_endpoint_reachable(webui_url)
|
||||
if gateway_ready and webui_ready:
|
||||
|
||||
@@ -102,6 +102,17 @@ class GatewayRuntime(ManagedProcessRuntime[ProcessStartOptions]):
|
||||
def _build_child_command(self, options: ProcessStartOptions) -> list[str]:
|
||||
return build_gateway_command(self.python_executable, options)
|
||||
|
||||
def restart(self, options: ProcessStartOptions, *, timeout_s: int = 20) -> ProcessResult:
|
||||
"""Restart an existing gateway without creating a new persistent instance."""
|
||||
with self._lifecycle_lock():
|
||||
status = self.status()
|
||||
if not status.running:
|
||||
return ProcessResult(False, "gateway_not_running", status)
|
||||
stop_result = self._stop(timeout_s=timeout_s)
|
||||
if not stop_result.ok:
|
||||
return stop_result
|
||||
return self._start_background(options)
|
||||
|
||||
|
||||
class GatewayClientLease:
|
||||
"""Reference-count an on-demand gateway shared by local interactive clients."""
|
||||
@@ -144,12 +155,14 @@ class GatewayClientLease:
|
||||
state["auto_stop"] = True
|
||||
self._write_state(state)
|
||||
|
||||
def mark_persistent(self) -> None:
|
||||
"""Keep an explicitly backgrounded gateway alive without client leases."""
|
||||
def mark_persistent(self) -> bool:
|
||||
"""Keep an explicitly backgrounded gateway alive; return whether it was promoted."""
|
||||
with self.lock:
|
||||
state = self._live_state()
|
||||
promoted = bool(state.get("auto_stop"))
|
||||
state["auto_stop"] = False
|
||||
self._write_or_clear(state)
|
||||
return promoted
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Forget leases after an explicit gateway stop."""
|
||||
|
||||
+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