Files
myagents/tests/test_ensure_backend.py
Zhengshou Lai 3f79301eb3 feat: ensure-agent + resolve npm-global CLIs without PATH
Add guided multi-select ensure-agent, resolve backend binaries via npm
prefix when not on PATH, and optionally persist npm bin in shell rc.
2026-08-09 16:53:06 +08:00

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()