fix(update): scope install-method stamp to the code tree, not $HERMES_HOME (#48188)
The install method (docker/git/pip/...) describes the *running binary*, but
detect_install_method() read it from $HERMES_HOME/.install_method — a shared
DATA directory. The Docker docs deliberately bind-mount $HERMES_HOME
(~/.hermes:/opt/data) so config/sessions/memory persist and can be shared with
a host-side Desktop/CLI install.
When a containerized gateway and a host install share one $HERMES_HOME, the
home-scoped stamp is a single slot describing two installs: the published image
stamps 'docker' on every boot, the host install then reads 'docker' and the
in-app updater refuses to run 'hermes update' ("doesn't apply inside the Docker
container"). Reinstalling the Desktop app from the DMG doesn't help because the
contaminated stamp is re-read every time.
Fix (option 1 — code-scoped stamp):
- detect_install_method() reads <install tree>/.install_method first (next to
the running code, immune to the shared data dir). It falls back to the legacy
$HERMES_HOME stamp for back-compat, but IGNORES a 'docker' home stamp when
not actually containerized — so already-poisoned shared homes self-heal.
- stamp_install_method() writes the code-scoped stamp.
- install.sh stamps $INSTALL_DIR instead of $HERMES_HOME.
- Dockerfile bakes 'docker' into /opt/hermes/.install_method at build time
(inside the immutable block); stage2-hook.sh no longer writes the home stamp
and proactively removes a stale 'docker' one to heal existing shared homes.
Genuine containers still resolve to 'docker' (baked stamp, or legacy home stamp
honored when containerized). Unstamped installs in generic containers still fall
through to git/pip (preserves the #34397 fix).
This commit is contained in:
@@ -48,6 +48,97 @@ def test_stamp_file_takes_precedence(tmp_path):
|
||||
assert detect_install_method(project_root=tmp_path) == "docker"
|
||||
|
||||
|
||||
def test_code_scoped_stamp_wins_over_home_stamp(tmp_path):
|
||||
"""The stamp next to the running code is authoritative over $HERMES_HOME.
|
||||
|
||||
Models a host git install whose $HERMES_HOME is shared with (and stamped
|
||||
'docker' by) a co-located container. The code-scoped stamp must win so the
|
||||
host install is correctly identified as 'git' and 'hermes update' works.
|
||||
"""
|
||||
code = tmp_path / "code"
|
||||
home = tmp_path / "home"
|
||||
code.mkdir()
|
||||
home.mkdir()
|
||||
(code / ".install_method").write_text("git\n")
|
||||
(home / ".install_method").write_text("docker\n") # container contamination
|
||||
with patch("hermes_cli.config.get_managed_system", return_value=None), \
|
||||
patch("hermes_cli.config.get_hermes_home", return_value=home):
|
||||
from hermes_cli.config import detect_install_method
|
||||
assert detect_install_method(project_root=code) == "git"
|
||||
|
||||
|
||||
def test_home_docker_stamp_ignored_when_not_containerized(tmp_path):
|
||||
"""A 'docker' home stamp is ignored on a host (non-container) install.
|
||||
|
||||
Self-heal path for homes already poisoned by an older image that wrote
|
||||
'docker' into the shared $HERMES_HOME. With no code-scoped stamp, a host
|
||||
git checkout must fall through to '.git' detection rather than honour the
|
||||
contaminating 'docker' value and refuse to update.
|
||||
"""
|
||||
code = tmp_path / "code"
|
||||
home = tmp_path / "home"
|
||||
code.mkdir()
|
||||
home.mkdir()
|
||||
(code / ".git").mkdir()
|
||||
(home / ".install_method").write_text("docker\n")
|
||||
with patch("hermes_cli.config.get_managed_system", return_value=None), \
|
||||
patch("hermes_cli.config.get_hermes_home", return_value=home), \
|
||||
patch("hermes_cli.config._running_in_container", return_value=False):
|
||||
from hermes_cli.config import detect_install_method
|
||||
assert detect_install_method(project_root=code) == "git"
|
||||
|
||||
|
||||
def test_home_docker_stamp_honored_inside_container(tmp_path):
|
||||
"""A 'docker' home stamp is still honoured when genuinely containerized.
|
||||
|
||||
Back-compat: an older published image that only ever wrote the home-scoped
|
||||
stamp (no baked code stamp) must still resolve to 'docker' so the update
|
||||
path keeps directing the user to ``docker pull``.
|
||||
"""
|
||||
code = tmp_path / "code"
|
||||
home = tmp_path / "home"
|
||||
code.mkdir()
|
||||
home.mkdir()
|
||||
(home / ".install_method").write_text("docker\n")
|
||||
with patch("hermes_cli.config.get_managed_system", return_value=None), \
|
||||
patch("hermes_cli.config.get_hermes_home", return_value=home), \
|
||||
patch("hermes_cli.config._running_in_container", return_value=True):
|
||||
from hermes_cli.config import detect_install_method
|
||||
assert detect_install_method(project_root=code) == "docker"
|
||||
|
||||
|
||||
def test_home_non_docker_stamp_still_honored_for_backcompat(tmp_path):
|
||||
"""Legacy non-'docker' home stamps (e.g. 'git') are still respected.
|
||||
|
||||
Only the 'docker' value carries the cross-contamination risk, so a host
|
||||
install that historically stamped 'git'/'pip' into $HERMES_HOME keeps
|
||||
resolving from there when no code-scoped stamp exists yet.
|
||||
"""
|
||||
code = tmp_path / "code"
|
||||
home = tmp_path / "home"
|
||||
code.mkdir()
|
||||
home.mkdir()
|
||||
(home / ".install_method").write_text("git\n")
|
||||
with patch("hermes_cli.config.get_managed_system", return_value=None), \
|
||||
patch("hermes_cli.config.get_hermes_home", return_value=home), \
|
||||
patch("hermes_cli.config._running_in_container", return_value=False):
|
||||
from hermes_cli.config import detect_install_method
|
||||
assert detect_install_method(project_root=code) == "git"
|
||||
|
||||
|
||||
def test_stamp_install_method_writes_code_scoped(tmp_path):
|
||||
"""stamp_install_method writes next to the code, not into $HERMES_HOME."""
|
||||
code = tmp_path / "code"
|
||||
home = tmp_path / "home"
|
||||
code.mkdir()
|
||||
home.mkdir()
|
||||
with patch("hermes_cli.config.get_hermes_home", return_value=home):
|
||||
from hermes_cli.config import stamp_install_method
|
||||
stamp_install_method("pip", project_root=code)
|
||||
assert (code / ".install_method").read_text().strip() == "pip"
|
||||
assert not (home / ".install_method").exists()
|
||||
|
||||
|
||||
def test_container_without_stamp_is_not_docker(tmp_path):
|
||||
"""An unstamped install in a generic container must NOT be flagged as docker.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user