feat: 优化运行时判定

This commit is contained in:
richarjiang
2026-09-24 14:27:07 +08:00
parent dfd3998399
commit f0ac603f27
25 changed files with 2368 additions and 58 deletions

176
tests/test_cli.py Normal file
View File

@@ -0,0 +1,176 @@
"""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)