fix(update): stop stash/restore from clobbering desktop source on managed clones (#38542)
The stash/restore cycle in the update path was observed to clobber freshly-pulled source files (apps/desktop/ deletion -> Vite '[UNRESOLVED_ENTRY] Cannot resolve entry module index.html'). On a managed clone the user never edits the source tree, so any 'dirty' state is pure git artifact (CRLF renormalization, npm lockfile churn, files left behind when a directory was deleted upstream such as apps/bootstrap-installer/). Stashing that and re-applying it after a pull is fragile and unnecessary. - hermes update (hermes_cli/main.py): on a non-fork (managed) clone, discard working-tree dirt via reset --hard HEAD + clean -fd instead of stash/apply. Forks keep the stash machinery so intentional edits survive. Also pin core.autocrlf=false on Windows so the dirt is never created (mirrors install.ps1 #38239). - install.sh: replace the update-path stash/restore dance with a hard reset to origin/<branch>; the installer is a managed-only entry point. - install.sh + install.ps1 desktop stage: prefer 'npm ci' (wipes and reinstalls node_modules from the lockfile) over bare 'npm install', which can report 'up to date' against a stale marker while node_modules is empty -- leaving tsc unresolved so 'npm run pack' fails. Tests: managed clone cleans instead of stashing; fork still stashes; existing stash tests force the stash path explicitly.
This commit is contained in:
@@ -597,6 +597,10 @@ def test_cmd_update_restores_stash_and_branch_when_already_up_to_date(monkeypatc
|
||||
hermes_main, "_stash_local_changes_if_needed",
|
||||
lambda *a, **kw: "abc123deadbeef",
|
||||
)
|
||||
# Force the stash path (not the managed-clone clean path) so this test
|
||||
# exercises stash restore. A real fork, or a clone where the managed
|
||||
# clean fails, falls through to stash.
|
||||
monkeypatch.setattr(hermes_main, "_clean_managed_worktree", lambda *a, **kw: False)
|
||||
restore_calls = []
|
||||
monkeypatch.setattr(
|
||||
hermes_main, "_restore_stashed_changes",
|
||||
@@ -635,6 +639,81 @@ def test_cmd_update_no_checkout_when_already_on_main(monkeypatch, tmp_path):
|
||||
assert len(checkout_calls) == 0
|
||||
|
||||
|
||||
def test_cmd_update_managed_clone_cleans_instead_of_stashing(monkeypatch, tmp_path):
|
||||
"""On a non-fork (managed) clone, working-tree dirt is discarded via
|
||||
_clean_managed_worktree, NOT preserved via stash/restore.
|
||||
|
||||
The stash/restore cycle has clobbered freshly-pulled source files
|
||||
(apps/desktop/ deletion → [UNRESOLVED_ENTRY] index.html). A managed clone
|
||||
has nothing the user authored, so the correct move is to throw the
|
||||
git-artifact dirt away and pull cleanly.
|
||||
"""
|
||||
_setup_update_mocks(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/uv" if name == "uv" else None)
|
||||
# Official origin → not a fork.
|
||||
monkeypatch.setattr(
|
||||
hermes_main, "_get_origin_url",
|
||||
lambda *a, **kw: "https://github.com/NousResearch/hermes-agent.git",
|
||||
)
|
||||
clean_calls = []
|
||||
monkeypatch.setattr(
|
||||
hermes_main, "_clean_managed_worktree",
|
||||
lambda *a, **kw: clean_calls.append(1) or True,
|
||||
)
|
||||
stash_calls = []
|
||||
monkeypatch.setattr(
|
||||
hermes_main, "_stash_local_changes_if_needed",
|
||||
lambda *a, **kw: stash_calls.append(1) or "shouldnotbeused",
|
||||
)
|
||||
restore_calls = []
|
||||
monkeypatch.setattr(
|
||||
hermes_main, "_restore_stashed_changes",
|
||||
lambda *a, **kw: restore_calls.append(1) or True,
|
||||
)
|
||||
|
||||
side_effect, _ = _make_update_side_effect(commit_count="0")
|
||||
monkeypatch.setattr(hermes_main.subprocess, "run", side_effect)
|
||||
|
||||
hermes_main.cmd_update(SimpleNamespace())
|
||||
|
||||
# Managed clean path used; stash path never touched.
|
||||
assert len(clean_calls) == 1
|
||||
assert len(stash_calls) == 0
|
||||
assert len(restore_calls) == 0
|
||||
|
||||
|
||||
def test_cmd_update_fork_still_uses_stash(monkeypatch, tmp_path):
|
||||
"""A fork (non-official origin) keeps the stash machinery so the user's
|
||||
intentional local edits survive the update."""
|
||||
_setup_update_mocks(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/uv" if name == "uv" else None)
|
||||
monkeypatch.setattr(
|
||||
hermes_main, "_get_origin_url",
|
||||
lambda *a, **kw: "https://github.com/someuser/hermes-agent.git",
|
||||
)
|
||||
clean_calls = []
|
||||
monkeypatch.setattr(
|
||||
hermes_main, "_clean_managed_worktree",
|
||||
lambda *a, **kw: clean_calls.append(1) or True,
|
||||
)
|
||||
stash_calls = []
|
||||
monkeypatch.setattr(
|
||||
hermes_main, "_stash_local_changes_if_needed",
|
||||
lambda *a, **kw: stash_calls.append(1) or "abc123",
|
||||
)
|
||||
monkeypatch.setattr(hermes_main, "_restore_stashed_changes", lambda *a, **kw: True)
|
||||
monkeypatch.setattr(hermes_main, "_sync_with_upstream_if_needed", lambda *a, **kw: None)
|
||||
|
||||
side_effect, _ = _make_update_side_effect(commit_count="0")
|
||||
monkeypatch.setattr(hermes_main.subprocess, "run", side_effect)
|
||||
|
||||
hermes_main.cmd_update(SimpleNamespace())
|
||||
|
||||
# Fork: stash path used, managed clean NOT used.
|
||||
assert len(stash_calls) == 1
|
||||
assert len(clean_calls) == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fetch failure — friendly error messages
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -685,6 +764,9 @@ def test_cmd_update_skips_stash_restore_when_reset_fails(monkeypatch, tmp_path,
|
||||
hermes_main, "_stash_local_changes_if_needed",
|
||||
lambda *a, **kw: "abc123deadbeef",
|
||||
)
|
||||
# Force the stash path so this test exercises the reset-failure handling
|
||||
# of the stash branch (not the managed-clone clean path).
|
||||
monkeypatch.setattr(hermes_main, "_clean_managed_worktree", lambda *a, **kw: False)
|
||||
restore_calls = []
|
||||
monkeypatch.setattr(
|
||||
hermes_main, "_restore_stashed_changes",
|
||||
|
||||
Reference in New Issue
Block a user