"""Tests for myagents.launcher.ensure_backend / guided auto-install.""" from __future__ import annotations import os from pathlib import Path from unittest.mock import patch from myagents import launcher as launcher_mod class TestEnsureBackend: def test_already_available_skips_install(self) -> None: with ( patch.object( launcher_mod, "_resolve_backend_binary", return_value="/usr/bin/claude", ), patch.object(launcher_mod, "_offer_install") as offer, ): assert launcher_mod.ensure_backend("claude") is True offer.assert_not_called() def test_noninteractive_auto_installs(self) -> None: with ( patch.object( launcher_mod, "_resolve_backend_binary", return_value=None ), patch.object(launcher_mod, "_is_interactive", return_value=False), patch.object( launcher_mod, "_run_install", return_value="/opt/bin/claude" ) as run_install, patch.object(launcher_mod, "click") as click_mod, ): assert launcher_mod.ensure_backend("claude") is True run_install.assert_called_once() click_mod.prompt.assert_not_called() def test_yes_true_skips_prompt(self) -> None: with ( patch.object( launcher_mod, "_resolve_backend_binary", return_value=None ), patch.object( launcher_mod, "_run_install", return_value="/opt/bin/claude" ) as run_install, patch.object(launcher_mod, "click") as click_mod, ): assert launcher_mod.ensure_backend("claude", yes=True) is True run_install.assert_called_once() click_mod.prompt.assert_not_called() def test_unknown_backend_false(self) -> None: assert launcher_mod.ensure_backend("nope") is False def test_hermes_cannot_auto_install(self) -> None: with ( patch.object( launcher_mod, "_resolve_backend_binary", return_value=None ), patch.object(launcher_mod, "_is_interactive", return_value=False), ): assert launcher_mod.ensure_backend("hermes") is False class TestResolveBackendBinary: def test_falls_back_to_npm_global_bin(self, tmp_path: Path, monkeypatch) -> None: bin_dir = tmp_path / "bin" bin_dir.mkdir() claude = bin_dir / "claude" claude.write_text("#!/bin/sh\n") claude.chmod(0o755) monkeypatch.delenv("CLAUDE_BIN", raising=False) with ( patch.object(launcher_mod.shutil, "which", return_value=None), patch.object(launcher_mod, "_npm_global_bin", return_value=bin_dir), ): assert launcher_mod.resolve_backend_binary("claude") == str(claude) def test_ensure_npm_bin_prepends_path( self, tmp_path: Path, monkeypatch ) -> None: bin_dir = tmp_path / "npm-bin" bin_dir.mkdir() monkeypatch.setenv("PATH", "/usr/bin") with patch.object(launcher_mod, "_npm_global_bin", return_value=bin_dir): got = launcher_mod.ensure_npm_bin_on_path(persist=False) assert got == bin_dir assert str(bin_dir) in os.environ["PATH"].split(os.pathsep) assert os.environ["PATH"].split(os.pathsep)[0] == str(bin_dir) class TestOfferInstallGuided: def test_interactive_default_yes_when_npm_present(self) -> None: cfg = launcher_mod._BACKENDS["claude"] with ( patch.object(launcher_mod, "_is_interactive", return_value=True), patch.object( launcher_mod.shutil, "which", return_value="/usr/bin/npm" ), patch.object( launcher_mod, "_run_install", return_value="/opt/bin/claude" ) as run_install, patch.object( launcher_mod.click, "prompt", return_value="y", ) as prompt, ): assert ( launcher_mod._offer_install(cfg, yes=None) == "/opt/bin/claude" ) prompt.assert_called_once() assert prompt.call_args.kwargs.get("default") == "y" run_install.assert_called_once()