diff --git a/.gitignore b/.gitignore index 5b3c3b1c15720..2948963310456 100644 --- a/.gitignore +++ b/.gitignore @@ -150,6 +150,11 @@ docs/superpowers/* .update-incomplete .update-incomplete.lock +# Installer-written method stamp in the managed checkout root (scripts/install.sh). +# Runtime metadata only — never a code change. Ignore so `git status` stays clean +# and `hermes update`'s untracked autostash does not treat it as a local edit (#66189 / #54855). +/.install_method + # Tool Search live-test harness output — non-deterministic model transcripts, # regenerated by scripts/tool_search_livetest.py. Never an artifact of the repo. scripts/out/ diff --git a/tests/hermes_cli/test_update_autostash.py b/tests/hermes_cli/test_update_autostash.py index be1a5f1acf4c0..5146320482187 100644 --- a/tests/hermes_cli/test_update_autostash.py +++ b/tests/hermes_cli/test_update_autostash.py @@ -901,3 +901,53 @@ def test_bootstrap_marker_not_autostashed_by_update(tmp_path): ["git", "status", "--porcelain"], cwd=tmp_path, capture_output=True, text=True ).stdout assert ".hermes-bootstrap-complete" not in status + + +def test_install_method_marker_not_autostashed_by_update(tmp_path): + """#66189: the installer ``.install_method`` stamp must be git-ignored so + ``hermes update``'s ``git stash push --include-untracked`` does not sweep it + into an autostash on every run. + + ``scripts/install.sh`` writes ``$INSTALL_DIR/.install_method`` as runtime + metadata; it is a sibling of ``.hermes-bootstrap-complete`` / + ``.update-incomplete`` and must be ignored the same way. Behavioral + + hermetic: adopt the project's real ``.gitignore`` (the contract under test), + drop the marker, and confirm the exact stash invocation the updater uses + leaves it untouched. + """ + import shutil + import subprocess + + if shutil.which("git") is None: + pytest.skip("git not available") + + repo_gitignore = Path(hermes_main.__file__).resolve().parents[1] / ".gitignore" + + def git(*args): + return subprocess.run( + ["git", *args], cwd=tmp_path, capture_output=True, text=True, check=True + ) + + git("init", "-q") + git("config", "user.email", "t@example.com") + git("config", "user.name", "t") + (tmp_path / ".gitignore").write_text(repo_gitignore.read_text()) + (tmp_path / "tracked.txt").write_text("x\n") + git("add", "-A") + git("commit", "-qm", "init") + + marker = tmp_path / ".install_method" + marker.write_text("managed\n") + + # Exact flags used by hermes update (hermes_cli/main.py). + git("stash", "push", "--include-untracked", "-m", "hermes-update-autostash") + + assert marker.exists(), ( + ".install_method was swept into the update autostash — it must be listed " + "in .gitignore so `git stash -u` skips it (#66189)." + ) + # It must not even register as a dirty/untracked change. + status = subprocess.run( + ["git", "status", "--porcelain"], cwd=tmp_path, capture_output=True, text=True + ).stdout + assert ".install_method" not in status