Files
aiturk-hermes-ide/tests/hermes_cli/test_update_venv_ownership_preflight.py
T

138 lines
4.7 KiB
Python

"""Venv ownership preflight for ``hermes update`` (#83529).
A venv touched by ``sudo pip`` / ``sudo hermes`` contains root-owned files
(e.g. ``site-packages/hermes_agent-*.dist-info/INSTALLER``). A later normal
``hermes update`` then dies mid-mutation inside ``uv pip install -e .``
("Permission denied (os error 13)") with ``venv/bin/hermes`` already deleted,
bricking the CLI. The preflight refuses BEFORE the first venv mutation and
prints the exact chown recovery command.
The helper must be pure ``os.stat``/``os.scandir`` — no subprocess calls —
because update-path tests mock ``subprocess.run`` with sequenced side effects.
"""
import os
import pytest
from hermes_cli import update_cmd
def _make_fake_venv(tmp_path):
"""Minimal POSIX venv layout with a dist-info directory."""
venv = tmp_path / "venv"
(venv / "bin").mkdir(parents=True)
(venv / "bin" / "hermes").write_text("#!stub\n")
(venv / "bin" / "python").write_text("#!stub\n")
sp = venv / "lib" / "python3.12" / "site-packages"
dist_info = sp / "hermes_agent-1.0.0.dist-info"
dist_info.mkdir(parents=True)
(dist_info / "INSTALLER").write_text("pip\n")
(dist_info / "RECORD").write_text("\n")
(sp / "some_pkg").mkdir()
return venv
def test_all_owned_returns_empty(tmp_path):
venv = _make_fake_venv(tmp_path)
assert update_cmd._venv_foreign_owned_paths(venv) == []
def test_all_owned_preflight_proceeds(tmp_path, monkeypatch, capsys):
"""Gate is a no-op (no exit, no output) when everything is user-owned."""
_make_fake_venv(tmp_path)
update_cmd._refuse_update_if_venv_foreign_owned(tmp_path)
assert capsys.readouterr().out == ""
def test_foreign_owned_dist_info_child_detected(tmp_path, monkeypatch):
venv = _make_fake_venv(tmp_path)
installer = str(
venv / "lib" / "python3.12" / "site-packages"
/ "hermes_agent-1.0.0.dist-info" / "INSTALLER"
)
real_uid = update_cmd._path_uid
def fake_uid(path):
if str(path) == installer:
return 0 # simulate root-owned sudo-pip residue
return real_uid(path)
monkeypatch.setattr(update_cmd, "_path_uid", fake_uid)
foreign = update_cmd._venv_foreign_owned_paths(venv)
assert foreign == [(installer, 0)]
def test_foreign_owned_refuses_with_chown_hint(tmp_path, monkeypatch, capsys):
venv = _make_fake_venv(tmp_path)
hermes_bin = str(venv / "bin" / "hermes")
real_uid = update_cmd._path_uid
monkeypatch.setattr(
update_cmd,
"_path_uid",
lambda p: 0 if str(p) == hermes_bin else real_uid(p),
)
with pytest.raises(SystemExit) as exc:
update_cmd._refuse_update_if_venv_foreign_owned(tmp_path)
assert exc.value.code == 1
out = capsys.readouterr().out
assert hermes_bin in out
assert "owner uid 0" in out
assert f"sudo chown -R $(id -un): {tmp_path}" in out
assert "Nothing in the venv was modified." in out
def test_limit_caps_reported_paths(tmp_path, monkeypatch):
venv = _make_fake_venv(tmp_path)
bin_dir = venv / "bin"
for i in range(10):
(bin_dir / f"tool{i}").write_text("x")
monkeypatch.setattr(
update_cmd,
"_path_uid",
lambda p: 0 if str(p).startswith(str(bin_dir) + os.sep) else 12345,
)
monkeypatch.setattr(update_cmd.os, "geteuid", lambda: 12345, raising=False)
foreign = update_cmd._venv_foreign_owned_paths(venv, limit=3)
assert len(foreign) == 3
def test_no_geteuid_returns_empty(tmp_path, monkeypatch):
"""Windows (no os.geteuid) skips the preflight entirely."""
venv = _make_fake_venv(tmp_path)
class _NoGeteuidOS:
def __getattr__(self, name):
if name == "geteuid":
raise AttributeError(name)
return getattr(os, name)
monkeypatch.setattr(update_cmd, "os", _NoGeteuidOS())
assert update_cmd._venv_foreign_owned_paths(venv) == []
def test_running_as_root_returns_empty(tmp_path, monkeypatch):
venv = _make_fake_venv(tmp_path)
monkeypatch.setattr(update_cmd.os, "geteuid", lambda: 0, raising=False)
# Even with foreign uids everywhere, root skips the gate.
monkeypatch.setattr(update_cmd, "_path_uid", lambda p: 4242)
assert update_cmd._venv_foreign_owned_paths(venv) == []
def test_never_raises_on_scandir_permission_error(tmp_path, monkeypatch):
venv = _make_fake_venv(tmp_path)
def boom(path):
raise PermissionError(13, "Permission denied", str(path))
monkeypatch.setattr(update_cmd.os, "scandir", boom)
assert update_cmd._venv_foreign_owned_paths(venv) == []
def test_never_raises_on_missing_venv(tmp_path):
assert update_cmd._venv_foreign_owned_paths(tmp_path / "no-venv") == []
def test_path_uid_none_on_oserror(tmp_path):
assert update_cmd._path_uid(tmp_path / "does-not-exist") is None