mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
fix(desktop): recover from corrupt cached Electron download on build
hermes desktop failed on Linux with an ENOENT renaming release/linux-unpacked/electron -> Hermes. Root cause is a corrupt cached Electron zip (~/.cache/electron/electron-*.zip): app-builder unpack-electron extracts a partial tree from the bad zip that is missing the electron binary, so electron-builder dies on the final rename. Re-running repeats the broken extraction, leaving the desktop app permanently unlaunchable until the cache is manually purged. - Add _electron_download_cache_dirs() + _purge_corrupt_electron_cache() to hermes_cli/main.py: validate every electron-*.zip via zipfile.testzip() and delete corrupt ones; honor electron_config_cache / ELECTRON_CACHE overrides with per-OS defaults. - Wire purge + single retry into cmd_gui packaged-build failure path so a poisoned download self-heals (electron re-downloads clean). - Add beforePack hook (apps/desktop/scripts/before-pack.cjs) to wipe the target unpacked dir before staging, making packaging idempotent across interrupted runs. Cross-platform, best-effort. - Tests: corrupt-zip detector, cmd_gui purge/retry/launch path, no-retry-when-clean path, and node --test for the cleanup helper.
This commit is contained in:
parent
e003c53b06
commit
f583c6ebd5
5 changed files with 311 additions and 0 deletions
|
|
@ -411,3 +411,91 @@ def test_compute_desktop_content_hash_respects_gitignore(tmp_path, monkeypatch):
|
|||
cli_main._DESKTOP_STAMP_SPEC = None
|
||||
h3 = cli_main._compute_desktop_content_hash(root)
|
||||
assert h1 != h3, "changing a tracked file should change the hash"
|
||||
|
||||
|
||||
|
||||
def _write_zip(path: Path) -> None:
|
||||
import zipfile
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(path, "w") as zf:
|
||||
zf.writestr("electron", "fake binary payload")
|
||||
|
||||
|
||||
def test_purge_corrupt_electron_cache_removes_only_bad_zips(tmp_path, monkeypatch):
|
||||
cache = tmp_path / "electron-cache"
|
||||
good = cache / "electron-v40.9.3-linux-x64.zip"
|
||||
bad = cache / "hashdir" / "electron-v40.9.3-linux-x64.zip"
|
||||
_write_zip(good)
|
||||
_write_zip(bad)
|
||||
# Corrupt the second zip by truncating its central directory.
|
||||
bad.write_bytes(bad.read_bytes()[:20])
|
||||
|
||||
monkeypatch.setattr(cli_main, "_electron_download_cache_dirs", lambda: [cache])
|
||||
|
||||
removed = cli_main._purge_corrupt_electron_cache()
|
||||
|
||||
assert removed == [bad]
|
||||
assert good.exists()
|
||||
assert not bad.exists()
|
||||
|
||||
|
||||
def test_purge_corrupt_electron_cache_noop_when_all_valid(tmp_path, monkeypatch):
|
||||
cache = tmp_path / "electron-cache"
|
||||
good = cache / "electron-v40.9.3-linux-x64.zip"
|
||||
_write_zip(good)
|
||||
monkeypatch.setattr(cli_main, "_electron_download_cache_dirs", lambda: [cache])
|
||||
|
||||
assert cli_main._purge_corrupt_electron_cache() == []
|
||||
assert good.exists()
|
||||
|
||||
|
||||
def test_gui_retries_pack_after_purging_corrupt_electron_cache(tmp_path, monkeypatch):
|
||||
root = _make_desktop_tree(tmp_path)
|
||||
desktop_dir = root / "apps" / "desktop"
|
||||
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
|
||||
packaged_exe = _make_packaged_executable(root, monkeypatch, platform="linux")
|
||||
|
||||
install_ok = subprocess.CompletedProcess(["npm", "ci"], 0)
|
||||
pack_fail = subprocess.CompletedProcess(["npm", "run", "pack"], 1)
|
||||
pack_ok = subprocess.CompletedProcess(["npm", "run", "pack"], 0)
|
||||
launch_ok = subprocess.CompletedProcess([str(packaged_exe)], 0)
|
||||
|
||||
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
|
||||
patch("hermes_cli.main._run_npm_install_deterministic", return_value=install_ok), \
|
||||
patch("hermes_cli.main._desktop_macos_relaunchable_fixup"), \
|
||||
patch("hermes_cli.main._purge_corrupt_electron_cache", return_value=[Path("/c/electron.zip")]) as mock_purge, \
|
||||
patch("hermes_cli.main.subprocess.run", side_effect=[pack_fail, pack_ok, launch_ok]) as mock_run, \
|
||||
pytest.raises(SystemExit) as exc:
|
||||
cli_main.cmd_gui(_ns())
|
||||
|
||||
assert exc.value.code == 0
|
||||
mock_purge.assert_called_once()
|
||||
# First pack fails, purge runs, second pack succeeds, then launch.
|
||||
assert mock_run.call_count == 3
|
||||
assert mock_run.call_args_list[0].args[0] == ["/usr/bin/npm", "run", "pack"]
|
||||
assert mock_run.call_args_list[1].args[0] == ["/usr/bin/npm", "run", "pack"]
|
||||
assert mock_run.call_args_list[2].args[0] == [str(packaged_exe)]
|
||||
|
||||
|
||||
def test_gui_does_not_retry_when_cache_is_clean(tmp_path, monkeypatch, capsys):
|
||||
root = _make_desktop_tree(tmp_path)
|
||||
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
|
||||
_make_packaged_executable(root, monkeypatch, platform="linux")
|
||||
|
||||
install_ok = subprocess.CompletedProcess(["npm", "ci"], 0)
|
||||
pack_fail = subprocess.CompletedProcess(["npm", "run", "pack"], 1)
|
||||
|
||||
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
|
||||
patch("hermes_cli.main._run_npm_install_deterministic", return_value=install_ok), \
|
||||
patch("hermes_cli.main._desktop_macos_relaunchable_fixup"), \
|
||||
patch("hermes_cli.main._purge_corrupt_electron_cache", return_value=[]) as mock_purge, \
|
||||
patch("hermes_cli.main.subprocess.run", side_effect=[pack_fail]) as mock_run, \
|
||||
pytest.raises(SystemExit) as exc:
|
||||
cli_main.cmd_gui(_ns())
|
||||
|
||||
assert exc.value.code == 1
|
||||
# Purge was attempted but found nothing, so no retry pack runs.
|
||||
mock_purge.assert_called_once()
|
||||
assert mock_run.call_count == 1
|
||||
assert "Desktop GUI build failed" in capsys.readouterr().out
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue