diff --git a/.gitignore b/.gitignore index b0abe3140b1..1efce4b83f1 100644 --- a/.gitignore +++ b/.gitignore @@ -108,6 +108,12 @@ docs/superpowers/* # logs, and per-session caches are never artifacts of the codebase. .hermes/ +# Desktop/bootstrap install marker written into the managed checkout root by the +# bootstrap installer. It is Hermes-managed runtime state, never a code change — +# ignore it so `hermes update`'s `git stash push --include-untracked` does not +# treat it as a local edit and autostash it on every run (#38529). +.hermes-bootstrap-complete + # 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 3ba19965591..16946834ab1 100644 --- a/tests/hermes_cli/test_update_autostash.py +++ b/tests/hermes_cli/test_update_autostash.py @@ -819,3 +819,50 @@ def test_cmd_update_skips_stash_restore_when_reset_fails(monkeypatch, tmp_path, out = capsys.readouterr().out assert "preserved in stash" in out + + +def test_bootstrap_marker_not_autostashed_by_update(tmp_path): + """#38529: the Desktop bootstrap marker must be git-ignored so that + ``hermes update``'s ``git stash push --include-untracked`` does not sweep it + into an autostash on every run. + + Behavioral + hermetic: build a throwaway repo that adopts the project's real + ``.gitignore`` (the contract under test), drop the marker, and confirm the + same 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 / ".hermes-bootstrap-complete" + marker.write_text("") + + # Exact flags used by hermes update (hermes_cli/main.py). + git("stash", "push", "--include-untracked", "-m", "hermes-update-autostash") + + assert marker.exists(), ( + ".hermes-bootstrap-complete was swept into the update autostash — it must " + "be listed in .gitignore so `git stash -u` skips it (#38529)." + ) + # 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 ".hermes-bootstrap-complete" not in status