import os import plistlib from nanobot.gateway import GatewayStartOptions from nanobot.gateway.service import GatewayServiceInstaller, GatewayServiceOptions def _expected_launchd_domain() -> str: getuid = getattr(os, "getuid", None) if getuid is None: return "gui/current" return f"gui/{getuid()}" def test_systemd_install_dry_run_renders_user_unit(tmp_path): installer = GatewayServiceInstaller(platform_name="Linux", home=tmp_path) result = installer.install( GatewayServiceOptions( start=GatewayStartOptions( port=18790, verbose=True, workspace="/tmp/nanobot workspace", config_path="/tmp/nanobot/config.json", ), python_executable="/venv/bin/python", ), dry_run=True, ) assert result.ok is True assert result.manager == "systemd" assert result.path == tmp_path / ".config/systemd/user/nanobot-gateway.service" assert ("systemctl", "--user", "daemon-reload") in result.commands assert ("systemctl", "--user", "enable", "nanobot-gateway.service") in result.commands assert ("systemctl", "--user", "restart", "nanobot-gateway.service") in result.commands assert result.content is not None assert 'WorkingDirectory="/tmp/nanobot workspace"' in result.content assert 'ExecStart=/venv/bin/python -m nanobot gateway --foreground --port 18790 --verbose' in result.content assert '--workspace "/tmp/nanobot workspace" --config /tmp/nanobot/config.json' in result.content def test_systemd_install_writes_unit_and_runs_commands(tmp_path): commands: list[list[str]] = [] workspace = tmp_path / "missing-workspace" installer = GatewayServiceInstaller( platform_name="Linux", home=tmp_path, subprocess_run=lambda command, **_kwargs: commands.append(command), ) result = installer.install( GatewayServiceOptions( start=GatewayStartOptions(port=18790, workspace=str(workspace)), enable=False, start_now=True, python_executable="/python", ) ) assert result.ok is True assert result.path is not None assert result.path.exists() assert workspace.exists() assert commands == [ ["systemctl", "--user", "daemon-reload"], ["systemctl", "--user", "restart", "nanobot-gateway.service"], ] def test_launchd_install_dry_run_renders_plist(tmp_path): installer = GatewayServiceInstaller(platform_name="Darwin", home=tmp_path) result = installer.install( GatewayServiceOptions( start=GatewayStartOptions( port=18791, workspace="/Users/test/.nanobot/workspace", config_path="/Users/test/.nanobot/config.json", ), python_executable="/opt/homebrew/bin/python3", ), dry_run=True, ) assert result.ok is True assert result.manager == "launchd" assert result.path == tmp_path / "Library/LaunchAgents/ai.nanobot.gateway.plist" assert result.content is not None payload = plistlib.loads(result.content.encode("utf-8")) assert payload["Label"] == "ai.nanobot.gateway" assert payload["ProgramArguments"] == [ "/opt/homebrew/bin/python3", "-m", "nanobot", "gateway", "--foreground", "--port", "18791", "--workspace", "/Users/test/.nanobot/workspace", "--config", "/Users/test/.nanobot/config.json", ] assert payload["KeepAlive"] == {"SuccessfulExit": False} assert payload["RunAtLoad"] is True assert ("launchctl", "bootstrap", _expected_launchd_domain(), str(result.path)) in result.commands def test_launchd_no_enable_start_still_bootstraps(tmp_path): installer = GatewayServiceInstaller(platform_name="Darwin", home=tmp_path) result = installer.install( GatewayServiceOptions( start=GatewayStartOptions(port=18790), enable=False, start_now=True, ), dry_run=True, ) assert result.content is not None payload = plistlib.loads(result.content.encode("utf-8")) assert payload["RunAtLoad"] is False assert result.commands[0][:2] == ("launchctl", "bootstrap") assert not any(command[1] == "enable" for command in result.commands) assert any(command[1] == "kickstart" for command in result.commands) def test_launchd_enable_without_start_sets_run_at_load_without_bootstrap(tmp_path): installer = GatewayServiceInstaller(platform_name="Darwin", home=tmp_path) result = installer.install( GatewayServiceOptions( start=GatewayStartOptions(port=18790), enable=True, start_now=False, ), dry_run=True, ) assert result.content is not None payload = plistlib.loads(result.content.encode("utf-8")) assert payload["RunAtLoad"] is True assert not any(command[1] == "bootstrap" for command in result.commands) assert any(command[1] == "enable" for command in result.commands) assert not any(command[1] == "kickstart" for command in result.commands) def test_launchd_no_enable_start_reinstall_boots_out_existing_label(tmp_path): commands: list[list[str]] = [] installer = GatewayServiceInstaller( platform_name="Darwin", home=tmp_path, subprocess_run=lambda command, **_kwargs: commands.append(command), ) result = installer.install( GatewayServiceOptions( start=GatewayStartOptions(port=18790), enable=False, start_now=True, ) ) assert result.ok is True assert commands[0][:2] == ["launchctl", "bootout"] assert commands[1][:2] == ["launchctl", "bootstrap"] def test_launchd_dry_run_does_not_require_posix_getuid(tmp_path, monkeypatch): monkeypatch.delattr(os, "getuid", raising=False) installer = GatewayServiceInstaller(platform_name="Darwin", home=tmp_path) result = installer.install( GatewayServiceOptions(start=GatewayStartOptions(port=18790)), dry_run=True, ) assert result.ok is True assert result.commands[0][:3] == ("launchctl", "bootstrap", "gui/current") def test_uninstall_systemd_removes_unit_and_reloads(tmp_path): commands: list[list[str]] = [] installer = GatewayServiceInstaller( platform_name="Linux", home=tmp_path, subprocess_run=lambda command, **_kwargs: commands.append(command), ) unit = tmp_path / ".config/systemd/user/nanobot-gateway.service" unit.parent.mkdir(parents=True) unit.write_text("[Unit]\n", encoding="utf-8") result = installer.uninstall() assert result.ok is True assert not unit.exists() assert commands == [ ["systemctl", "--user", "disable", "--now", "nanobot-gateway.service"], ["systemctl", "--user", "daemon-reload"], ] def test_auto_manager_rejects_windows_services(tmp_path): installer = GatewayServiceInstaller(platform_name="Windows", home=tmp_path) result = installer.install( GatewayServiceOptions(start=GatewayStartOptions(port=18790)), dry_run=True, ) assert result.ok is False assert result.message == "unsupported_service_manager:windows"