mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
fix(gateway): preserve shared runtime identity
This commit is contained in:
+25
-17
@@ -14,8 +14,8 @@ from rich.console import Console
|
|||||||
|
|
||||||
from nanobot.config.schema import Config
|
from nanobot.config.schema import Config
|
||||||
from nanobot.gateway import (
|
from nanobot.gateway import (
|
||||||
|
GatewayInstance,
|
||||||
GatewayRuntime,
|
GatewayRuntime,
|
||||||
GatewayRuntimePaths,
|
|
||||||
GatewayStartOptions,
|
GatewayStartOptions,
|
||||||
GatewayStatus,
|
GatewayStatus,
|
||||||
)
|
)
|
||||||
@@ -78,19 +78,21 @@ def create_gateway_app(
|
|||||||
filter=lambda record: record["extra"].setdefault("channel", "-") or True,
|
filter=lambda record: record["extra"].setdefault("channel", "-") or True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def instance_for_selectors(
|
||||||
|
*,
|
||||||
|
workspace: str | None = None,
|
||||||
|
config: str | None = None,
|
||||||
|
) -> GatewayInstance:
|
||||||
|
return GatewayInstance.resolve(
|
||||||
|
config_path=_resolved_config_selector(config),
|
||||||
|
workspace=workspace,
|
||||||
|
)
|
||||||
|
|
||||||
def runtime_for_instance(*, workspace: str | None = None, config: str | None = None):
|
def runtime_for_instance(*, workspace: str | None = None, config: str | None = None):
|
||||||
if runtime_factory is not None:
|
if runtime_factory is not None:
|
||||||
return runtime_factory(workspace=workspace, config=config)
|
return runtime_factory(workspace=workspace, config=config)
|
||||||
resolved_config = _resolved_config_selector(config)
|
instance = instance_for_selectors(workspace=workspace, config=config)
|
||||||
config_path = str(resolved_config)
|
return GatewayRuntime(paths=instance.paths)
|
||||||
workspace_path = str(Path(workspace).expanduser().resolve(strict=False)) if workspace else None
|
|
||||||
return GatewayRuntime(
|
|
||||||
paths=GatewayRuntimePaths.for_instance(
|
|
||||||
data_dir=resolved_config.parent,
|
|
||||||
workspace=workspace_path,
|
|
||||||
config_path=config_path,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def service_installer():
|
def service_installer():
|
||||||
return service_factory() if service_factory is not None else GatewayServiceInstaller()
|
return service_factory() if service_factory is not None else GatewayServiceInstaller()
|
||||||
@@ -109,13 +111,12 @@ def create_gateway_app(
|
|||||||
loaded_config: Config | None = None,
|
loaded_config: Config | None = None,
|
||||||
) -> GatewayStartOptions:
|
) -> GatewayStartOptions:
|
||||||
cfg = loaded_config or load_runtime_config(config, workspace)
|
cfg = loaded_config or load_runtime_config(config, workspace)
|
||||||
resolved_config = str(_resolved_config_selector(config)) if config else None
|
return instance_for_selectors(
|
||||||
resolved_workspace = str(Path(workspace).expanduser().resolve(strict=False)) if workspace else None
|
workspace=workspace,
|
||||||
return GatewayStartOptions(
|
config=config,
|
||||||
|
).start_options(
|
||||||
port=port if port is not None else cfg.gateway.port,
|
port=port if port is not None else cfg.gateway.port,
|
||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
workspace=resolved_workspace,
|
|
||||||
config_path=resolved_config,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def print_status(status: GatewayStatus) -> None:
|
def print_status(status: GatewayStatus) -> None:
|
||||||
@@ -235,17 +236,24 @@ def create_gateway_app(
|
|||||||
|
|
||||||
configure_logging(verbose)
|
configure_logging(verbose)
|
||||||
cfg = load_runtime_config(config, workspace)
|
cfg = load_runtime_config(config, workspace)
|
||||||
|
instance = instance_for_selectors(workspace=workspace, config=config)
|
||||||
unconfigured_provider_error = None
|
unconfigured_provider_error = None
|
||||||
if validate_startup_config is not None:
|
if validate_startup_config is not None:
|
||||||
unconfigured_provider_error = validate_startup_config(cfg)
|
unconfigured_provider_error = validate_startup_config(cfg)
|
||||||
if unconfigured_provider_error is None:
|
if unconfigured_provider_error is None:
|
||||||
run_gateway(cfg, port=port, webui_bundle_mode=interactive_build_mode())
|
run_gateway(
|
||||||
|
cfg,
|
||||||
|
port=port,
|
||||||
|
webui_bundle_mode=interactive_build_mode(),
|
||||||
|
gateway_instance=instance,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
run_gateway(
|
run_gateway(
|
||||||
cfg,
|
cfg,
|
||||||
port=port,
|
port=port,
|
||||||
webui_bundle_mode=interactive_build_mode(),
|
webui_bundle_mode=interactive_build_mode(),
|
||||||
unconfigured_provider_error=unconfigured_provider_error,
|
unconfigured_provider_error=unconfigured_provider_error,
|
||||||
|
gateway_instance=instance,
|
||||||
)
|
)
|
||||||
|
|
||||||
@gateway_app.command("status")
|
@gateway_app.command("status")
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from nanobot.cli.webui_support import (
|
|||||||
)
|
)
|
||||||
from nanobot.config.paths import is_default_workspace
|
from nanobot.config.paths import is_default_workspace
|
||||||
from nanobot.config.schema import Config
|
from nanobot.config.schema import Config
|
||||||
|
from nanobot.gateway.runtime import GatewayInstance
|
||||||
from nanobot.security.network import is_loopback_host
|
from nanobot.security.network import is_loopback_host
|
||||||
from nanobot.session.keys import UNIFIED_SESSION_KEY, last_channel_from_metadata
|
from nanobot.session.keys import UNIFIED_SESSION_KEY, last_channel_from_metadata
|
||||||
from nanobot.utils.evaluator import evaluate_response, resolve_evaluator_prompt
|
from nanobot.utils.evaluator import evaluate_response, resolve_evaluator_prompt
|
||||||
@@ -298,6 +299,7 @@ def _run_gateway(
|
|||||||
health_server_enabled: bool = True,
|
health_server_enabled: bool = True,
|
||||||
unconfigured_provider_error: str | None = None,
|
unconfigured_provider_error: str | None = None,
|
||||||
webui_dev_server: WebUIDevServer | None = None,
|
webui_dev_server: WebUIDevServer | None = None,
|
||||||
|
gateway_instance: GatewayInstance | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Shared gateway runtime; ``open_browser_url`` opens a tab once channels are up."""
|
"""Shared gateway runtime; ``open_browser_url`` opens a tab once channels are up."""
|
||||||
from nanobot.agent.model_presets import load_model_preset_catalog
|
from nanobot.agent.model_presets import load_model_preset_catalog
|
||||||
@@ -392,28 +394,15 @@ def _run_gateway(
|
|||||||
from nanobot.gateway.runtime import (
|
from nanobot.gateway.runtime import (
|
||||||
GatewayClientLease,
|
GatewayClientLease,
|
||||||
GatewayRuntime,
|
GatewayRuntime,
|
||||||
GatewayRuntimePaths,
|
|
||||||
GatewayStartOptions,
|
|
||||||
monitor_gateway_clients,
|
monitor_gateway_clients,
|
||||||
)
|
)
|
||||||
|
|
||||||
config_path = str(get_config_path().resolve(strict=False))
|
instance = gateway_instance or GatewayInstance.resolve(
|
||||||
gateway_workspace = (
|
config_path=get_config_path(),
|
||||||
str(config.workspace_path)
|
|
||||||
if not is_default_workspace(config.workspace_path)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
gateway_runtime = GatewayRuntime(
|
|
||||||
paths=GatewayRuntimePaths.for_instance(
|
|
||||||
workspace=gateway_workspace,
|
|
||||||
config_path=config_path,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
gateway_start_options = GatewayStartOptions(
|
|
||||||
port=port,
|
|
||||||
workspace=gateway_workspace,
|
|
||||||
config_path=config_path,
|
|
||||||
)
|
)
|
||||||
|
config_path = str(instance.config_path)
|
||||||
|
gateway_runtime = GatewayRuntime(paths=instance.paths)
|
||||||
|
gateway_start_options = instance.start_options(port=port)
|
||||||
|
|
||||||
# Preserve existing single-workspace installs, but keep custom workspaces clean.
|
# Preserve existing single-workspace installs, but keep custom workspaces clean.
|
||||||
if is_default_workspace(config.workspace_path):
|
if is_default_workspace(config.workspace_path):
|
||||||
|
|||||||
@@ -256,24 +256,16 @@ def _ensure_gateway(
|
|||||||
) -> _GatewayHandle:
|
) -> _GatewayHandle:
|
||||||
from nanobot.gateway import (
|
from nanobot.gateway import (
|
||||||
GatewayClientLease,
|
GatewayClientLease,
|
||||||
|
GatewayInstance,
|
||||||
GatewayRuntime,
|
GatewayRuntime,
|
||||||
GatewayRuntimePaths,
|
|
||||||
GatewayStartOptions,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
base_url = _webui_browser_url(config).split("/#/", 1)[0].rstrip("/")
|
base_url = _webui_browser_url(config).split("/#/", 1)[0].rstrip("/")
|
||||||
workspace_override_path = (
|
instance = GatewayInstance.resolve(
|
||||||
str(Path(workspace_override).expanduser().resolve(strict=False))
|
config_path=config_path,
|
||||||
if workspace_override
|
workspace=workspace_override,
|
||||||
else None
|
|
||||||
)
|
|
||||||
runtime = GatewayRuntime(
|
|
||||||
paths=GatewayRuntimePaths.for_instance(
|
|
||||||
data_dir=config_path.parent,
|
|
||||||
workspace=workspace_override_path,
|
|
||||||
config_path=str(config_path),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
runtime = GatewayRuntime(paths=instance.paths)
|
||||||
lease = GatewayClientLease(runtime, kind="tui")
|
lease = GatewayClientLease(runtime, kind="tui")
|
||||||
lease.acquire()
|
lease.acquire()
|
||||||
try:
|
try:
|
||||||
@@ -294,11 +286,7 @@ def _ensure_gateway(
|
|||||||
)
|
)
|
||||||
|
|
||||||
result = lease.ensure_on_demand_gateway(
|
result = lease.ensure_on_demand_gateway(
|
||||||
GatewayStartOptions(
|
instance.start_options(port=config.gateway.port)
|
||||||
port=config.gateway.port,
|
|
||||||
workspace=workspace_override_path,
|
|
||||||
config_path=str(config_path),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if not result.ok and result.message != "gateway_already_running":
|
if not result.ok and result.message != "gateway_already_running":
|
||||||
raise TuiUnavailableError(
|
raise TuiUnavailableError(
|
||||||
|
|||||||
+6
-15
@@ -101,9 +101,8 @@ def webui(
|
|||||||
from nanobot.config.loader import resolve_config_env_vars, save_config
|
from nanobot.config.loader import resolve_config_env_vars, save_config
|
||||||
from nanobot.gateway import (
|
from nanobot.gateway import (
|
||||||
GatewayClientLease,
|
GatewayClientLease,
|
||||||
|
GatewayInstance,
|
||||||
GatewayRuntime,
|
GatewayRuntime,
|
||||||
GatewayRuntimePaths,
|
|
||||||
GatewayStartOptions,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
cli_terminal._ensure_interactive_tty_mode()
|
cli_terminal._ensure_interactive_tty_mode()
|
||||||
@@ -223,20 +222,12 @@ def webui(
|
|||||||
mode="skip" if dev else webui_bundle_mode,
|
mode="skip" if dev else webui_bundle_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
config_arg = str(config_path)
|
instance = GatewayInstance.resolve(
|
||||||
workspace_arg = str(Path(workspace).expanduser().resolve(strict=False)) if workspace else None
|
config_path=config_path,
|
||||||
runtime = GatewayRuntime(
|
workspace=workspace,
|
||||||
paths=GatewayRuntimePaths.for_instance(
|
|
||||||
data_dir=config_path.parent,
|
|
||||||
workspace=workspace_arg,
|
|
||||||
config_path=config_arg,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
start_options = GatewayStartOptions(
|
|
||||||
port=effective_gateway_port,
|
|
||||||
workspace=workspace_arg,
|
|
||||||
config_path=config_arg,
|
|
||||||
)
|
)
|
||||||
|
runtime = GatewayRuntime(paths=instance.paths)
|
||||||
|
start_options = instance.start_options(port=effective_gateway_port)
|
||||||
|
|
||||||
def ensure_shared_gateway(*, client_lease: GatewayClientLease) -> None:
|
def ensure_shared_gateway(*, client_lease: GatewayClientLease) -> None:
|
||||||
"""Start or refresh the one managed gateway shared by local clients."""
|
"""Start or refresh the one managed gateway shared by local clients."""
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from nanobot.gateway.runtime import (
|
from nanobot.gateway.runtime import (
|
||||||
GatewayClientLease,
|
GatewayClientLease,
|
||||||
|
GatewayInstance,
|
||||||
GatewayRuntime,
|
GatewayRuntime,
|
||||||
GatewayRuntimePaths,
|
GatewayRuntimePaths,
|
||||||
GatewayStartOptions,
|
GatewayStartOptions,
|
||||||
@@ -12,6 +13,7 @@ from nanobot.gateway.runtime import (
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"GatewayClientLease",
|
"GatewayClientLease",
|
||||||
|
"GatewayInstance",
|
||||||
"GatewayRuntime",
|
"GatewayRuntime",
|
||||||
"GatewayRuntimePaths",
|
"GatewayRuntimePaths",
|
||||||
"GatewayStartOptions",
|
"GatewayStartOptions",
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ GatewayLaunchMode = Literal["foreground", "background", "unknown"]
|
|||||||
GatewayLifetime = Literal["explicit", "on_demand"]
|
GatewayLifetime = Literal["explicit", "on_demand"]
|
||||||
|
|
||||||
|
|
||||||
|
def _default_config_path() -> Path:
|
||||||
|
return (Path.home() / ".nanobot" / "config.json").resolve(strict=False)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GatewayStatus(ProcessStatus):
|
class GatewayStatus(ProcessStatus):
|
||||||
"""Observable lifecycle state for one shared local gateway."""
|
"""Observable lifecycle state for one shared local gateway."""
|
||||||
@@ -107,6 +111,56 @@ class GatewayRuntimePaths(ProcessRuntimePaths):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class GatewayInstance:
|
||||||
|
"""One stable local gateway identity and its child-process selectors."""
|
||||||
|
|
||||||
|
config_path: Path
|
||||||
|
workspace: str | None
|
||||||
|
paths: GatewayRuntimePaths
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
config_path: str | Path,
|
||||||
|
workspace: str | None = None,
|
||||||
|
) -> "GatewayInstance":
|
||||||
|
resolved_config = Path(config_path).expanduser().resolve(strict=False)
|
||||||
|
resolved_workspace = (
|
||||||
|
str(Path(workspace).expanduser().resolve(strict=False)) if workspace else None
|
||||||
|
)
|
||||||
|
# The released default instance used gateway.json. Keep that identity stable
|
||||||
|
# across upgrades while still namespacing explicit configs and workspaces.
|
||||||
|
config_selector = (
|
||||||
|
None if resolved_config == _default_config_path() else str(resolved_config)
|
||||||
|
)
|
||||||
|
return cls(
|
||||||
|
config_path=resolved_config,
|
||||||
|
workspace=resolved_workspace,
|
||||||
|
paths=GatewayRuntimePaths.for_instance(
|
||||||
|
data_dir=resolved_config.parent,
|
||||||
|
workspace=resolved_workspace,
|
||||||
|
config_path=config_selector,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_options(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
port: int,
|
||||||
|
verbose: bool = False,
|
||||||
|
) -> GatewayStartOptions:
|
||||||
|
return GatewayStartOptions(
|
||||||
|
port=port,
|
||||||
|
verbose=verbose,
|
||||||
|
workspace=self.workspace,
|
||||||
|
config_path=(
|
||||||
|
None if self.config_path == _default_config_path() else str(self.config_path)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GatewayRuntime(ManagedProcessRuntime[ProcessStartOptions]):
|
class GatewayRuntime(ManagedProcessRuntime[ProcessStartOptions]):
|
||||||
"""Manage a background ``nanobot gateway`` process."""
|
"""Manage a background ``nanobot gateway`` process."""
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ from typer.testing import CliRunner
|
|||||||
|
|
||||||
from nanobot.cli.gateway import _resolved_config_selector, create_gateway_app
|
from nanobot.cli.gateway import _resolved_config_selector, create_gateway_app
|
||||||
from nanobot.config.schema import Config
|
from nanobot.config.schema import Config
|
||||||
from nanobot.gateway import GatewayRuntimePaths, GatewayStartOptions, GatewayStatus, RuntimeResult
|
from nanobot.gateway import (
|
||||||
|
GatewayInstance,
|
||||||
|
GatewayRuntimePaths,
|
||||||
|
GatewayStartOptions,
|
||||||
|
GatewayStatus,
|
||||||
|
RuntimeResult,
|
||||||
|
)
|
||||||
from nanobot.gateway.service import GatewayServiceOptions, GatewayServiceResult
|
from nanobot.gateway.service import GatewayServiceOptions, GatewayServiceResult
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
@@ -111,7 +117,9 @@ def _test_app(
|
|||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
fake_runtime = FakeRuntime(tmp_path)
|
fake_runtime = FakeRuntime(tmp_path)
|
||||||
fake_service = FakeServiceInstaller(tmp_path)
|
fake_service = FakeServiceInstaller(tmp_path)
|
||||||
run_calls: list[tuple[Config, int | None, str | None, str | None]] = []
|
run_calls: list[
|
||||||
|
tuple[Config, int | None, str | None, str | None, GatewayInstance | None]
|
||||||
|
] = []
|
||||||
prepare_calls: list[tuple[Config, str]] = []
|
prepare_calls: list[tuple[Config, str]] = []
|
||||||
|
|
||||||
def load_runtime_config(_config_path: str | None, _workspace: str | None) -> Config:
|
def load_runtime_config(_config_path: str | None, _workspace: str | None) -> Config:
|
||||||
@@ -123,9 +131,10 @@ def _test_app(
|
|||||||
port: int | None = None,
|
port: int | None = None,
|
||||||
webui_bundle_mode: str | None = None,
|
webui_bundle_mode: str | None = None,
|
||||||
unconfigured_provider_error: str | None = None,
|
unconfigured_provider_error: str | None = None,
|
||||||
|
gateway_instance: GatewayInstance | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
run_calls.append(
|
run_calls.append(
|
||||||
(config, port, webui_bundle_mode, unconfigured_provider_error)
|
(config, port, webui_bundle_mode, unconfigured_provider_error, gateway_instance)
|
||||||
)
|
)
|
||||||
|
|
||||||
def prepare_webui_bundle(config: Config, mode: str) -> None:
|
def prepare_webui_bundle(config: Config, mode: str) -> None:
|
||||||
@@ -162,6 +171,22 @@ def test_gateway_default_still_runs_foreground(tmp_path):
|
|||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
assert calls[0][1] == 18791
|
assert calls[0][1] == 18791
|
||||||
assert calls[0][2] == "warn"
|
assert calls[0][2] == "warn"
|
||||||
|
assert calls[0][4] == GatewayInstance.resolve(
|
||||||
|
config_path=_resolved_config_selector(None)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_workspace_does_not_split_the_foreground_instance(tmp_path: Path) -> None:
|
||||||
|
config = Config()
|
||||||
|
config.agents.defaults.workspace = str(tmp_path / "configured-workspace")
|
||||||
|
app, _runtime, _service, calls, _prepare_calls = _test_app(tmp_path, config=config)
|
||||||
|
|
||||||
|
result = runner.invoke(app, ["gateway"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert calls[0][4] == GatewayInstance.resolve(
|
||||||
|
config_path=_resolved_config_selector(None)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_gateway_foreground_passes_recoverable_provider_error_to_runner(tmp_path):
|
def test_gateway_foreground_passes_recoverable_provider_error_to_runner(tmp_path):
|
||||||
@@ -213,7 +238,9 @@ def test_gateway_background_starts_detached_runtime(tmp_path):
|
|||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Gateway started in the background" in result.stdout
|
assert "Gateway started in the background" in result.stdout
|
||||||
assert fake_runtime.started_options == GatewayStartOptions(port=18792)
|
assert fake_runtime.started_options == GatewayInstance.resolve(
|
||||||
|
config_path=_resolved_config_selector(None)
|
||||||
|
).start_options(port=18792)
|
||||||
assert prepare_calls == [(config, "warn")]
|
assert prepare_calls == [(config, "warn")]
|
||||||
|
|
||||||
|
|
||||||
@@ -344,7 +371,9 @@ def test_gateway_restart_starts_background_runtime(tmp_path):
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Gateway restarted in the background" in result.stdout
|
assert "Gateway restarted in the background" in result.stdout
|
||||||
assert fake_runtime.stop_timeout == 9
|
assert fake_runtime.stop_timeout == 9
|
||||||
assert fake_runtime.restarted_options == GatewayStartOptions(port=18793, verbose=True)
|
assert fake_runtime.restarted_options == GatewayInstance.resolve(
|
||||||
|
config_path=_resolved_config_selector(None)
|
||||||
|
).start_options(port=18793, verbose=True)
|
||||||
assert prepare_calls == [(config, "warn")]
|
assert prepare_calls == [(config, "warn")]
|
||||||
assert json.loads(lease_state.read_text(encoding="utf-8"))["auto_stop"] is True
|
assert json.loads(lease_state.read_text(encoding="utf-8"))["auto_stop"] is True
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import pytest
|
|||||||
|
|
||||||
from nanobot.gateway import (
|
from nanobot.gateway import (
|
||||||
GatewayClientLease,
|
GatewayClientLease,
|
||||||
|
GatewayInstance,
|
||||||
GatewayRuntime,
|
GatewayRuntime,
|
||||||
GatewayRuntimePaths,
|
GatewayRuntimePaths,
|
||||||
GatewayStartOptions,
|
GatewayStartOptions,
|
||||||
@@ -58,6 +59,34 @@ def test_paths_use_stable_instance_suffix_for_custom_selectors(tmp_path):
|
|||||||
assert first_paths.log_path != second_paths.log_path
|
assert first_paths.log_path != second_paths.log_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_instance_preserves_released_gateway_paths() -> None:
|
||||||
|
config_path = Path.home() / ".nanobot" / "config.json"
|
||||||
|
|
||||||
|
instance = GatewayInstance.resolve(config_path=config_path)
|
||||||
|
|
||||||
|
assert instance.paths.state_path == config_path.parent / "run" / "gateway.json"
|
||||||
|
assert instance.paths.log_path == config_path.parent / "logs" / "gateway.log"
|
||||||
|
assert instance.start_options(port=18790) == GatewayStartOptions(port=18790)
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_instance_round_trips_the_same_child_selectors(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "instance" / "config.json"
|
||||||
|
workspace = tmp_path / "workspace"
|
||||||
|
parent = GatewayInstance.resolve(
|
||||||
|
config_path=config_path,
|
||||||
|
workspace=str(workspace),
|
||||||
|
)
|
||||||
|
options = parent.start_options(port=18790)
|
||||||
|
|
||||||
|
child = GatewayInstance.resolve(
|
||||||
|
config_path=options.config_path or "",
|
||||||
|
workspace=options.workspace,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert child == parent
|
||||||
|
assert parent.paths.state_path.name.startswith("gateway.")
|
||||||
|
|
||||||
|
|
||||||
def test_start_background_writes_state_and_child_command(tmp_path, monkeypatch):
|
def test_start_background_writes_state_and_child_command(tmp_path, monkeypatch):
|
||||||
calls: list[dict] = []
|
calls: list[dict] = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user