176 lines
5.8 KiB
Python
176 lines
5.8 KiB
Python
"""Tests for ``laya_router.cli`` PATH / symlink helpers.
|
|
|
|
These cover the path exposure logic added so ``laya-router`` and
|
|
``laya-hook`` work from any shell without manual ``PATH`` setup.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from laya_router.cli import (
|
|
_apply_path_symlinks,
|
|
_bin_link_targets,
|
|
_remove_path_symlinks,
|
|
_user_path_bin_dir,
|
|
ensure_path_symlinks,
|
|
remove_path_symlinks,
|
|
)
|
|
|
|
|
|
class _FakeSettings:
|
|
"""Minimal stand-in for ``Settings`` carrying just the paths we touch."""
|
|
|
|
def __init__(self, router: Path, hook: Path):
|
|
self.router_bin = router
|
|
self.hook_bin = hook
|
|
self.mcp_bin = router.with_name("laya-mcp")
|
|
|
|
|
|
def _make_fake_binaries(tmp_path: Path) -> tuple[Path, Path, _FakeSettings]:
|
|
"""Build two venv-style binaries on disk and return (router, hook, settings)."""
|
|
src_dir = tmp_path / "venv_bin"
|
|
src_dir.mkdir()
|
|
router = src_dir / "laya-router"
|
|
hook = src_dir / "laya-hook"
|
|
router.write_text("#!/bin/sh\necho router\n")
|
|
hook.write_text("#!/bin/sh\necho hook\n")
|
|
return router, hook, _FakeSettings(router, hook)
|
|
|
|
|
|
def test_bin_link_targets_returns_both_binaries():
|
|
from laya_router.config import load_settings
|
|
|
|
targets = _bin_link_targets(load_settings())
|
|
names = [name for name, _ in targets]
|
|
assert names == ["laya-router", "laya-hook", "laya-mcp"]
|
|
assert targets[0][1].is_file()
|
|
assert targets[1][1].is_file()
|
|
|
|
|
|
def test_user_path_bin_dir_prefers_local_bin(monkeypatch, tmp_path):
|
|
monkeypatch.setenv("PATH", f"{tmp_path}:/usr/bin")
|
|
preferred = Path.home() / ".local" / "bin"
|
|
if preferred.is_dir() and os.access(preferred, os.W_OK):
|
|
assert _user_path_bin_dir() == preferred
|
|
else:
|
|
assert _user_path_bin_dir() == tmp_path
|
|
|
|
|
|
def test_user_path_bin_dir_skips_system_dirs(monkeypatch):
|
|
# Force "no writable dir exists" by overriding to a non-existent path.
|
|
monkeypatch.setenv("LAYA_ROUTER_BIN_DIR", "/this/path/does/not/exist")
|
|
monkeypatch.setenv("PATH", "/usr/local/bin:/usr/bin:/bin:/sbin")
|
|
assert _user_path_bin_dir() is None
|
|
|
|
|
|
def test_user_path_bin_dir_respects_env_override(monkeypatch, tmp_path):
|
|
monkeypatch.setenv("LAYA_ROUTER_BIN_DIR", str(tmp_path))
|
|
assert _user_path_bin_dir() == tmp_path
|
|
|
|
|
|
def test_user_path_bin_dir_env_override_rejects_unwritable(monkeypatch, tmp_path):
|
|
monkeypatch.setenv("LAYA_ROUTER_BIN_DIR", "/this/path/does/not/exist")
|
|
assert _user_path_bin_dir() is None
|
|
|
|
|
|
def test_apply_path_symlinks_creates_links(tmp_path):
|
|
router, hook, settings = _make_fake_binaries(tmp_path)
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
|
|
_apply_path_symlinks(settings, bin_dir)
|
|
|
|
assert (bin_dir / "laya-router").is_symlink()
|
|
assert (bin_dir / "laya-router").resolve() == router.resolve()
|
|
assert (bin_dir / "laya-hook").is_symlink()
|
|
assert (bin_dir / "laya-hook").resolve() == hook.resolve()
|
|
|
|
|
|
def test_apply_path_symlinks_is_noop_when_correct(tmp_path):
|
|
_, _, settings = _make_fake_binaries(tmp_path)
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
|
|
_apply_path_symlinks(settings, bin_dir)
|
|
first = (bin_dir / "laya-router").lstat()
|
|
|
|
_apply_path_symlinks(settings, bin_dir)
|
|
second = (bin_dir / "laya-router").lstat()
|
|
|
|
# Re-running on a correct symlink must not rewrite it.
|
|
assert first.st_mtime_ns == second.st_mtime_ns
|
|
|
|
|
|
def test_apply_path_symlinks_repairs_wrong_target(tmp_path):
|
|
router, _, settings = _make_fake_binaries(tmp_path)
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
(bin_dir / "laya-router").symlink_to("/etc/passwd")
|
|
|
|
_apply_path_symlinks(settings, bin_dir)
|
|
assert (bin_dir / "laya-router").resolve() == router.resolve()
|
|
|
|
|
|
def test_apply_path_symlinks_repairs_broken_symlink(tmp_path):
|
|
router, _, settings = _make_fake_binaries(tmp_path)
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
(bin_dir / "laya-router").symlink_to("/nonexistent/path")
|
|
|
|
_apply_path_symlinks(settings, bin_dir)
|
|
assert (bin_dir / "laya-router").resolve() == router.resolve()
|
|
|
|
|
|
def test_apply_path_symlinks_skips_unrelated_file(tmp_path, capsys):
|
|
router, _, settings = _make_fake_binaries(tmp_path)
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
existing = bin_dir / "laya-router"
|
|
existing.write_text("user's binary")
|
|
|
|
_apply_path_symlinks(settings, bin_dir)
|
|
|
|
captured = capsys.readouterr()
|
|
assert "已被占用" in captured.err
|
|
# The user's file must not be clobbered.
|
|
assert existing.read_text() == "user's binary"
|
|
# And the other one should still get a fresh symlink.
|
|
assert (bin_dir / "laya-hook").is_symlink()
|
|
assert (bin_dir / "laya-router").resolve() != router.resolve()
|
|
|
|
|
|
def test_remove_path_symlinks_removes_both(tmp_path):
|
|
_, _, settings = _make_fake_binaries(tmp_path)
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
_apply_path_symlinks(settings, bin_dir)
|
|
assert (bin_dir / "laya-router").is_symlink()
|
|
assert (bin_dir / "laya-hook").is_symlink()
|
|
|
|
_remove_path_symlinks(settings, bin_dir)
|
|
assert not (bin_dir / "laya-router").exists()
|
|
assert not (bin_dir / "laya-hook").exists()
|
|
|
|
|
|
def test_remove_path_symlinks_idempotent(tmp_path):
|
|
_, _, settings = _make_fake_binaries(tmp_path)
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
|
|
# No symlinks created — remove should still be safe.
|
|
_remove_path_symlinks(settings, bin_dir)
|
|
_remove_path_symlinks(settings, bin_dir)
|
|
|
|
|
|
def test_public_helpers_warn_when_no_writable_path(monkeypatch, capsys):
|
|
monkeypatch.setenv("LAYA_ROUTER_BIN_DIR", "/this/path/does/not/exist")
|
|
settings = _FakeSettings(Path("/nonexistent/router"), Path("/nonexistent/hook"))
|
|
|
|
ensure_path_symlinks(settings)
|
|
captured = capsys.readouterr()
|
|
assert "没有可写的目录" in captured.err
|
|
|
|
# remove_path_symlinks is silent in the same situation.
|
|
remove_path_symlinks(settings) |