fix(updater): rebuild desktop on Windows hand-off repair path
The HERMES_UPDATE_REEXEC child and the current-checkout Node repair path printed success without calling _rebuild_desktop_after_update. A failed rebuild now withholds the success banner the same way the commits-pulled path does. Fixes #97343
This commit is contained in:
@@ -4126,6 +4126,7 @@ def _repair_node_deps_on_current_checkout(
|
|||||||
gateway_mode: bool = False,
|
gateway_mode: bool = False,
|
||||||
pre_update_snapshot_id: str | None = None,
|
pre_update_snapshot_id: str | None = None,
|
||||||
completion_message: str = "✓ Already up to date!",
|
completion_message: str = "✓ Already up to date!",
|
||||||
|
had_desktop_app_before_update: bool = False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Repair Node deps on the ``commit_count == 0`` path (#77211).
|
"""Repair Node deps on the ``commit_count == 0`` path (#77211).
|
||||||
|
|
||||||
@@ -4156,6 +4157,23 @@ def _repair_node_deps_on_current_checkout(
|
|||||||
gateway_mode=gateway_mode,
|
gateway_mode=gateway_mode,
|
||||||
pre_update_snapshot_id=pre_update_snapshot_id,
|
pre_update_snapshot_id=pre_update_snapshot_id,
|
||||||
)
|
)
|
||||||
|
# A current checkout can still owe a Desktop rebuild (#97343): the
|
||||||
|
# packaged app is built from source the pull already landed — or, on the
|
||||||
|
# Windows hand-off, by a child that never reaches the commits-pulled
|
||||||
|
# rebuild. Skipping it leaves a stale desktop app behind a
|
||||||
|
# successful-looking update. Self-gates on the build stamp, so this is a
|
||||||
|
# no-op when nothing changed.
|
||||||
|
if not _rebuild_desktop_after_update(
|
||||||
|
_m().PROJECT_ROOT / "apps" / "desktop",
|
||||||
|
had_desktop_app_before_update=had_desktop_app_before_update,
|
||||||
|
):
|
||||||
|
# _rebuild_desktop_after_update already printed the retry hint; withhold
|
||||||
|
# success rather than claiming the update finished (#88251).
|
||||||
|
print_completion(
|
||||||
|
"⚠ Update partially complete — the desktop app was not rebuilt "
|
||||||
|
"and is still on the previous build."
|
||||||
|
)
|
||||||
|
return False
|
||||||
return bool(print_completion(completion_message))
|
return bool(print_completion(completion_message))
|
||||||
|
|
||||||
|
|
||||||
@@ -8647,9 +8665,24 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
|||||||
gateway_mode=gateway_mode,
|
gateway_mode=gateway_mode,
|
||||||
pre_update_snapshot_id=pre_update_snapshot_id,
|
pre_update_snapshot_id=pre_update_snapshot_id,
|
||||||
)
|
)
|
||||||
current_checkout_complete = _print_verified_update_completion(
|
# The Windows hand-off child lands here after doing the
|
||||||
"✓ Update complete!"
|
# sync its parent could not, and the commits-pulled
|
||||||
)
|
# rebuild below is never reached — rebuild the Desktop
|
||||||
|
# app here or it silently stays on the old build
|
||||||
|
# (#97343).
|
||||||
|
if _rebuild_desktop_after_update(
|
||||||
|
desktop_dir,
|
||||||
|
had_desktop_app_before_update=had_desktop_app_before_update,
|
||||||
|
):
|
||||||
|
current_checkout_complete = _print_verified_update_completion(
|
||||||
|
"✓ Update complete!"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
current_checkout_complete = False
|
||||||
|
_print_update_completion(
|
||||||
|
"⚠ Update partially complete — the desktop app was "
|
||||||
|
"not rebuilt and is still on the previous build."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
current_checkout_complete = False
|
current_checkout_complete = False
|
||||||
print(f"⚠ Venv still unhealthy after repair: {detail_after}")
|
print(f"⚠ Venv still unhealthy after repair: {detail_after}")
|
||||||
@@ -8665,6 +8698,7 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
|||||||
if upstream_checked
|
if upstream_checked
|
||||||
else "✓ Up to date with your fork (official repo not checked)."
|
else "✓ Up to date with your fork (official repo not checked)."
|
||||||
),
|
),
|
||||||
|
had_desktop_app_before_update=had_desktop_app_before_update,
|
||||||
)
|
)
|
||||||
if runtime_repaired is not None and not _m()._is_windows():
|
if runtime_repaired is not None and not _m()._is_windows():
|
||||||
print()
|
print()
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ def test_repair_node_deps_runs_config_migration_on_version_bump(capsys):
|
|||||||
"_run_migrate_config_fresh",
|
"_run_migrate_config_fresh",
|
||||||
return_value={"env_added": [], "config_added": ["migrated to v38"], "warnings": []},
|
return_value={"env_added": [], "config_added": ["migrated to v38"], "warnings": []},
|
||||||
) as mock_migrate,
|
) as mock_migrate,
|
||||||
|
patch.object(update_cmd, "_rebuild_desktop_after_update", return_value=True),
|
||||||
):
|
):
|
||||||
update_cmd._repair_node_deps_on_current_checkout(completion)
|
update_cmd._repair_node_deps_on_current_checkout(completion)
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ def test_repair_node_deps_up_to_date_config(capsys):
|
|||||||
patch("hermes_cli.config.get_missing_env_vars", return_value=[]),
|
patch("hermes_cli.config.get_missing_env_vars", return_value=[]),
|
||||||
patch("hermes_cli.config.get_missing_config_fields", return_value=[]),
|
patch("hermes_cli.config.get_missing_config_fields", return_value=[]),
|
||||||
patch.object(update_cmd, "_run_migrate_config_fresh") as mock_migrate,
|
patch.object(update_cmd, "_run_migrate_config_fresh") as mock_migrate,
|
||||||
|
patch.object(update_cmd, "_rebuild_desktop_after_update", return_value=True),
|
||||||
):
|
):
|
||||||
update_cmd._repair_node_deps_on_current_checkout(completion)
|
update_cmd._repair_node_deps_on_current_checkout(completion)
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ def test_current_checkout_healthy_node_deps_reports_up_to_date():
|
|||||||
completion = MagicMock()
|
completion = MagicMock()
|
||||||
with patch.object(
|
with patch.object(
|
||||||
update_cmd, "_update_node_dependencies", return_value=[]
|
update_cmd, "_update_node_dependencies", return_value=[]
|
||||||
), patch.object(update_cmd, "_m") as m:
|
), patch.object(update_cmd, "_m") as m, patch.object(
|
||||||
|
update_cmd, "_rebuild_desktop_after_update", return_value=True
|
||||||
|
):
|
||||||
update_cmd._repair_node_deps_on_current_checkout(completion)
|
update_cmd._repair_node_deps_on_current_checkout(completion)
|
||||||
|
|
||||||
# The refresh pairs with the web build like every other call site.
|
# The refresh pairs with the web build like every other call site.
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""The current-checkout repair path must rebuild the Desktop app (#97343).
|
||||||
|
|
||||||
|
A Windows git install runs `hermes update` from `hermes.exe`, which reexecs a
|
||||||
|
venv-Python child to finish the dependency sync. That child completes through
|
||||||
|
``_repair_node_deps_on_current_checkout`` / the hand-off repair branch, never
|
||||||
|
through the commits-pulled path that owns the Desktop rebuild — so a
|
||||||
|
successful-looking update left the packaged desktop app on the previous build.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from hermes_cli import update_cmd
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_checkout_repair_rebuilds_desktop_under_project_root():
|
||||||
|
"""The repair passes PROJECT_ROOT/apps/desktop and the pre-update flag."""
|
||||||
|
completion = MagicMock(return_value=True)
|
||||||
|
with (
|
||||||
|
patch.object(update_cmd, "_update_node_dependencies", return_value=[]),
|
||||||
|
patch.object(update_cmd, "_m") as m,
|
||||||
|
patch.object(update_cmd, "_check_and_apply_config_migration"),
|
||||||
|
patch.object(
|
||||||
|
update_cmd, "_rebuild_desktop_after_update", return_value=True
|
||||||
|
) as rebuild,
|
||||||
|
):
|
||||||
|
m.return_value.PROJECT_ROOT = update_cmd.Path("/fake/hermes")
|
||||||
|
complete = update_cmd._repair_node_deps_on_current_checkout(
|
||||||
|
completion, had_desktop_app_before_update=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert complete is True
|
||||||
|
rebuild.assert_called_once()
|
||||||
|
assert rebuild.call_args[0][0] == update_cmd.Path("/fake/hermes/apps/desktop")
|
||||||
|
assert rebuild.call_args[1]["had_desktop_app_before_update"] is True
|
||||||
|
completion.assert_called_once_with("✓ Already up to date!")
|
||||||
|
|
||||||
|
|
||||||
|
def test_failed_desktop_rebuild_withholds_success_completion():
|
||||||
|
"""A failed rebuild must not report success and must return False."""
|
||||||
|
completion = MagicMock(return_value=True)
|
||||||
|
with (
|
||||||
|
patch.object(update_cmd, "_update_node_dependencies", return_value=[]),
|
||||||
|
patch.object(update_cmd, "_m") as m,
|
||||||
|
patch.object(update_cmd, "_check_and_apply_config_migration"),
|
||||||
|
patch.object(update_cmd, "_rebuild_desktop_after_update", return_value=False),
|
||||||
|
):
|
||||||
|
m.return_value.PROJECT_ROOT = update_cmd.Path("/fake/hermes")
|
||||||
|
complete = update_cmd._repair_node_deps_on_current_checkout(completion)
|
||||||
|
|
||||||
|
assert complete is False
|
||||||
|
for call in completion.call_args_list:
|
||||||
|
assert not call[0][0].startswith("✓")
|
||||||
@@ -70,6 +70,11 @@ def test_current_checkout_completion_is_verified_before_success(capsys, monkeypa
|
|||||||
def test_current_checkout_repair_returns_verified_completion_result(monkeypatch):
|
def test_current_checkout_repair_returns_verified_completion_result(monkeypatch):
|
||||||
monkeypatch.setattr(update_cmd, "_update_node_dependencies", lambda: [])
|
monkeypatch.setattr(update_cmd, "_update_node_dependencies", lambda: [])
|
||||||
monkeypatch.setattr(update_cmd._m(), "_build_web_ui", lambda _path: None)
|
monkeypatch.setattr(update_cmd._m(), "_build_web_ui", lambda _path: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
update_cmd,
|
||||||
|
"_rebuild_desktop_after_update",
|
||||||
|
lambda _dir, **_kwargs: True,
|
||||||
|
)
|
||||||
|
|
||||||
complete = update_cmd._repair_node_deps_on_current_checkout(
|
complete = update_cmd._repair_node_deps_on_current_checkout(
|
||||||
lambda _message: False
|
lambda _message: False
|
||||||
|
|||||||
Reference in New Issue
Block a user