From 01c14ad7f336c9fad67451e9fc6e3f149c2426e4 Mon Sep 17 00:00:00 2001 From: JonthanaHanh <92574114+JonthanaHanh@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:38:55 -0700 Subject: [PATCH] fix(update): ZIP swap preserves the built desktop app (apps/desktop/release) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #70337/#87331 win-unpacked wipe half, from PR #70477 by @JonthanaHanh (reimplemented against the two-phase staged swap that postdates that branch — the live release/ dir is grafted into the staged apps copy BEFORE the atomic commit, so preservation rides the same rollback machinery instead of a post-hoc copy). Co-authored-by: JonthanaHanh <92574114+JonthanaHanh@users.noreply.github.com> --- hermes_cli/update_cmd.py | 16 ++++++ .../test_update_zip_release_preserve.py | 56 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/hermes_cli/test_update_zip_release_preserve.py diff --git a/hermes_cli/update_cmd.py b/hermes_cli/update_cmd.py index 64cdb9b377..c380f24e9a 100644 --- a/hermes_cli/update_cmd.py +++ b/hermes_cli/update_cmd.py @@ -1522,6 +1522,22 @@ def _update_via_zip(args, *, had_desktop_app_before_update: bool = False) -> boo src = os.path.join(extracted, item) dst = os.path.join(str(_m().PROJECT_ROOT), item) staged.append((_stage_replacement(src, dst), dst)) + # #70337/#87331: the GitHub source ZIP contains only source — + # apps/desktop/release/ (the BUILT desktop app, win-unpacked/ + # Hermes.exe) exists only in the LIVE tree. Swapping `apps` + # without it deletes the desktop build and breaks the + # shortcut. Graft the live release dir into the staged copy + # BEFORE the swap so the commit preserves it atomically. + if item == "apps": + live_release = os.path.join(dst, "desktop", "release") + staged_release = os.path.join( + staged[-1][0], "desktop", "release" + ) + if os.path.isdir(live_release) and not os.path.exists( + staged_release + ): + os.makedirs(os.path.dirname(staged_release), exist_ok=True) + shutil.copytree(live_release, staged_release) except Exception: # Nothing is live yet; drop the partial staging copies so a retry # starts from the same free space this attempt did. diff --git a/tests/hermes_cli/test_update_zip_release_preserve.py b/tests/hermes_cli/test_update_zip_release_preserve.py new file mode 100644 index 0000000000..61478821a7 --- /dev/null +++ b/tests/hermes_cli/test_update_zip_release_preserve.py @@ -0,0 +1,56 @@ +"""#70337/#87331: the ZIP swap must preserve apps/desktop/release/. + +The GitHub source ZIP carries only source; the BUILT desktop app +(release/win-unpacked/Hermes.exe) exists only in the live tree. Swapping +`apps` without grafting the live release dir deletes the desktop build. +""" + +from __future__ import annotations + +import os +import shutil +from pathlib import Path + + +def test_staged_apps_swap_preserves_live_release_dir(tmp_path, monkeypatch): + from hermes_cli import main as hermes_main + from hermes_cli.update_cmd import ( + _commit_staged_replacements, + _stage_replacement, + ) + + # live tree: apps/desktop/release/win-unpacked/Hermes.exe + old source + root = tmp_path / "install" + live_apps = root / "apps" / "desktop" + (live_apps / "release" / "win-unpacked").mkdir(parents=True) + (live_apps / "release" / "win-unpacked" / "Hermes.exe").write_bytes(b"MZbuilt") + (live_apps / "electron").mkdir() + (live_apps / "electron" / "main.ts").write_text("old source") + + # extracted ZIP: new source, NO release dir (GitHub source archive shape) + extracted = tmp_path / "extracted" + zip_apps = extracted / "apps" / "desktop" + (zip_apps / "electron").mkdir(parents=True) + (zip_apps / "electron" / "main.ts").write_text("new source") + + monkeypatch.setattr(hermes_main, "PROJECT_ROOT", root) + + # Reproduce the _update_via_zip staging loop for the `apps` entry, + # including the release-dir graft. + src = str(extracted / "apps") + dst = str(root / "apps") + staged_path = _stage_replacement(src, dst) + live_release = os.path.join(dst, "desktop", "release") + staged_release = os.path.join(staged_path, "desktop", "release") + if os.path.isdir(live_release) and not os.path.exists(staged_release): + os.makedirs(os.path.dirname(staged_release), exist_ok=True) + shutil.copytree(live_release, staged_release) + + _commit_staged_replacements([(staged_path, dst)]) + + # New source landed AND the built desktop app survived. + assert (root / "apps" / "desktop" / "electron" / "main.ts").read_text() == ( + "new source" + ) + exe = root / "apps" / "desktop" / "release" / "win-unpacked" / "Hermes.exe" + assert exe.exists() and exe.read_bytes() == b"MZbuilt"