Add guided multi-select ensure-agent, resolve backend binaries via npm prefix when not on PATH, and optionally persist npm bin in shell rc.
117 lines
4.3 KiB
Python
117 lines
4.3 KiB
Python
"""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()
|