Offer install with default Yes on TTY; auto-install when non-interactive (install/CI). Resolve binaries from npm global prefix after install.
106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
"""Tests for myagents.launcher.ensure_backend / guided auto-install."""
|
|
|
|
from __future__ import annotations
|
|
|
|
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 TestResolveInstalledBinary:
|
|
def test_falls_back_to_npm_global_bin(self, tmp_path: Path) -> None:
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
claude = bin_dir / "claude"
|
|
claude.write_text("#!/bin/sh\n")
|
|
claude.chmod(0o755)
|
|
cfg = launcher_mod._BACKENDS["claude"]
|
|
with (
|
|
patch.object(
|
|
launcher_mod, "_resolve_backend_binary", return_value=None
|
|
),
|
|
patch.object(launcher_mod, "_npm_global_bin", return_value=bin_dir),
|
|
):
|
|
assert launcher_mod._resolve_installed_binary(cfg) == str(claude)
|
|
|
|
|
|
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()
|