from __future__ import annotations import json import os import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] def run_cli(tmp_path: Path, *args: str) -> subprocess.CompletedProcess[str]: env = os.environ.copy() for key in tuple(env): if key.startswith("NEXUS_"): env.pop(key) env["NEXUS_HOME"] = str(tmp_path / "state") env["NEXUS_CONFIG_DIR"] = str(tmp_path / "config") return subprocess.run( [sys.executable, "-m", "management.cli", *args], cwd=ROOT, env=env, text=True, capture_output=True, timeout=30, check=False, ) def test_help_exposes_portable_command_tree(tmp_path): result = run_cli(tmp_path, "--help") assert result.returncode == 0, result.stderr for command in ("init", "config", "provider", "doctor", "serve", "models", "chat"): assert command in result.stdout def test_legacy_cli_spellings_remain_compatible(): from management.cli import _normalize_legacy_argv assert _normalize_legacy_argv(["start", "-b"]) == ["start", "backend"] assert _normalize_legacy_argv(["stop", "--ai"]) == ["stop", "ai"] assert _normalize_legacy_argv(["logs", "-m", "--follow"]) == ["logs", "memory", "--follow"] assert _normalize_legacy_argv(["backup", "full"]) == ["backup", "--full"] assert _normalize_legacy_argv(["restore", "-f"]) == ["restore"] assert _normalize_legacy_argv(["help"]) == ["--help"] def test_init_uses_external_state_and_seeds_playbooks(tmp_path): result = run_cli(tmp_path, "init", "--json") assert result.returncode == 0, result.stderr payload = json.loads(result.stdout) assert Path(payload["state_dir"]) == (tmp_path / "state").resolve() assert payload["seeded_playbooks"] == len(list((ROOT / "data" / "playbooks").glob("*.yaml"))) assert len(list((tmp_path / "state" / "data" / "playbooks").glob("*.yaml"))) > 0 def test_config_persists_validated_values(tmp_path): set_result = run_cli(tmp_path, "config", "set", "backend_port", "8123", "--json") assert set_result.returncode == 0, set_result.stderr get_result = run_cli(tmp_path, "config", "get", "backend_port", "--json") assert get_result.returncode == 0, get_result.stderr assert json.loads(get_result.stdout)["backend_port"] == 8123 invalid = run_cli(tmp_path, "config", "set", "backend_port", "70000") assert invalid.returncode == 2 assert "between 1 and 65535" in invalid.stderr def test_remote_provider_configuration_is_explicit(tmp_path): result = run_cli( tmp_path, "provider", "use", "remote", "--url", "http://phone-lan:11434", "--json" ) assert result.returncode == 0, result.stderr payload = json.loads(result.stdout) assert payload["provider"] == "ollama-remote" assert payload["url"] == "http://phone-lan:11434" values = json.loads((tmp_path / "config" / "config.json").read_text(encoding="utf-8")) assert values["provider"] == "ollama-remote" show = run_cli(tmp_path, "provider", "show", "--json") assert show.returncode == 0, show.stderr assert json.loads(show.stdout)["url"] == "http://phone-lan:11434" def test_serve_rejects_invalid_ports_before_startup(tmp_path): result = run_cli(tmp_path, "serve", "--port", "0") assert result.returncode == 2 assert "between 1 and 65535" in result.stderr def test_remote_provider_is_never_stopped_or_force_killed(monkeypatch): from management import ncp killed_ports = [] killed_patterns = [] monkeypatch.setattr(ncp.settings, "manage_ollama", False) monkeypatch.setattr(ncp.settings, "ollama_host", "http://remote.test:11434") monkeypatch.setattr(ncp, "kill_port", lambda port: killed_ports.append(port) or False) monkeypatch.setattr( ncp, "kill_matching", lambda patterns, force=False: killed_patterns.extend(patterns) or 0 ) ncp.stop_ollama() ncp.cmd_kill() assert 11434 not in killed_ports assert "ollama serve" not in killed_patterns