Files
hermes-agent/tests/hermes_cli/test_ensure_hermes_home_uid_34107.py
T
ethernet 30da5d0a89 test: run os-specific tests on their real host, not a faked one
many tests patched sys.platform or a module's _IS_WINDOWS flag, then
ran on linux ci. the patch selects the branch under test, but the host
does not have the behavior the branch exists for. the test proves the
patch, not the platform. some gated assertions never ran on any host.

this commit adds three markers: linux_only, macos_only, windows_only.
a conftest hook skips a marked test on the other hosts, with a clear
reason. no test fakes a host now. two documented fakes remain
(android/termux, freebsd) because no ci runner exists for them.

each fake site got one of four treatments:
- gate it: the real host supplies the platform; mocks cover real
  dependencies only, never host identity
- patch the module's own probe when the subject is the probe's consumer
- assert against the real host when the fake stood in for any non-x host
- delete the patch when it set the value the host already has

bare skipif(sys.platform != ...) guards became markers too. the lane
model skips these on linux and never imports them on windows, so they
ran on no host. platform parametrize tables are now one marked test
per os.

running on real hosts found real errors: a chrome-sandbox failure in
test_gui_command that main hides, and two windows failures fixed here.
the agents.md testing section now documents the policy.
2026-08-09 22:09:49 -04:00

136 lines
5.1 KiB
Python

"""Regression tests for #34107 — Docker UID/GID handling in ensure_hermes_home.
When Hermes runs in Docker with ``HERMES_UID=1000`` / ``HERMES_GID=911``,
the entrypoint chowns the top-level ``HERMES_HOME`` once at startup. But
subdirectories created at runtime by ``ensure_hermes_home()`` — especially
for profile namespaces under ``profiles/<name>/`` spawned by kanban
workers — were landing as ``root:root`` and blocking subsequent
uid-mapped worker invocations with ``PermissionError [Errno 13]``.
The fix is a ``_chown_to_hermes_uid`` helper that reads the env vars and
applies chown after ``mkdir``, invoked from ``_secure_dir`` (which already
runs after every directory creation in the home-init path).
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
from unittest.mock import patch
import pytest
# ---------------------------------------------------------------------------
# _resolve_hermes_uid_gid
# ---------------------------------------------------------------------------
class TestResolveHermesUidGid:
def test_returns_parsed_values_when_both_set(self, monkeypatch):
monkeypatch.setenv("HERMES_UID", "1000")
monkeypatch.setenv("HERMES_GID", "911")
from hermes_cli.config import _resolve_hermes_uid_gid
uid, gid = _resolve_hermes_uid_gid()
assert uid == 1000
assert gid == 911
# ``windows_only`` rather than ``skipif(sys.platform != "win32")``: the
# Windows CI job selects ``-m windows_only``, so a bare skipif would leave
# this test skipped on Linux AND unselected on the Windows lane — dead on
# every host.
@pytest.mark.windows_only
def test_windows_returns_none_none(self, monkeypatch):
monkeypatch.setenv("HERMES_UID", "1000")
monkeypatch.setenv("HERMES_GID", "911")
from hermes_cli.config import _resolve_hermes_uid_gid
uid, gid = _resolve_hermes_uid_gid()
assert uid is None
assert gid is None
# ---------------------------------------------------------------------------
# _chown_to_hermes_uid
# ---------------------------------------------------------------------------
class TestChownToHermesUid:
def test_calls_os_chown_when_both_set(self, tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_UID", "1000")
monkeypatch.setenv("HERMES_GID", "911")
from hermes_cli import config as cfg
d = tmp_path / "subdir"
d.mkdir()
with patch.object(cfg.os, "chown") as mock_chown:
cfg._chown_to_hermes_uid(d)
mock_chown.assert_called_once_with(d, 1000, 911)
def test_eperm_is_silently_swallowed(self, tmp_path, monkeypatch):
"""When running as non-root, os.chown raises EPERM. That's fine —
the entrypoint's startup chown -R will pick it up on restart, and
in most cases the dir was already correctly-owned by the calling
user anyway."""
monkeypatch.setenv("HERMES_UID", "1000")
monkeypatch.setenv("HERMES_GID", "911")
from hermes_cli import config as cfg
d = tmp_path / "subdir"
d.mkdir()
def _raises_eperm(*args, **kwargs):
raise PermissionError("operation not permitted")
with patch.object(cfg.os, "chown", side_effect=_raises_eperm):
# Must not raise — the catch is non-fatal.
cfg._chown_to_hermes_uid(d)
def test_attributeerror_swallowed_for_windows_compat(self, tmp_path, monkeypatch):
"""os.chown doesn't exist on Windows. Catching AttributeError keeps
the helper portable."""
monkeypatch.setenv("HERMES_UID", "1000")
monkeypatch.setenv("HERMES_GID", "911")
from hermes_cli import config as cfg
d = tmp_path / "subdir"
d.mkdir()
with patch.object(cfg.os, "chown", side_effect=AttributeError("no chown on this platform")):
cfg._chown_to_hermes_uid(d) # must not raise
# ---------------------------------------------------------------------------
# End-to-end: _secure_dir now also chowns
# ---------------------------------------------------------------------------
class TestSecureDirChown:
@pytest.mark.skipif(sys.platform == "win32", reason="chown is no-op on Windows")
def test_secure_dir_invokes_chown_when_env_set(self, tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_UID", "1000")
monkeypatch.setenv("HERMES_GID", "911")
from hermes_cli import config as cfg
d = tmp_path / "subdir"
d.mkdir()
with patch.object(cfg.os, "chown") as mock_chown:
cfg._secure_dir(d)
mock_chown.assert_called_once_with(d, 1000, 911)
@pytest.mark.skipif(sys.platform == "win32", reason="chown is no-op on Windows")
def test_secure_dir_no_chown_when_env_unset(self, tmp_path, monkeypatch):
monkeypatch.delenv("HERMES_UID", raising=False)
monkeypatch.delenv("HERMES_GID", raising=False)
from hermes_cli import config as cfg
d = tmp_path / "subdir"
d.mkdir()
with patch.object(cfg.os, "chown") as mock_chown:
cfg._secure_dir(d)
mock_chown.assert_not_called()