Files
hermes-agent/tests/hermes_cli/test_update_completion_routing.py
ethernet cc1b5e2add fix(update): validate late upstream sync before restoring local work
Move the code-changing fork sync into the protected pull transaction.
Retain the origin movement/branch gate before syncing, then validate final
syntax and HEAD before restoring, discarding or parking the autostash.
Syntax failures now reset to the pre-update SHA while local staged,
unstaged and untracked work stays recoverable in the original stash.

Extend real Git coverage to late sync with accepted/refused origin pushes,
post-sync reset/branch faults, and dirty syntax rollback. Remove the fake
second-move test now covered by those real transactions; retain native
Windows trampoline rows with platform markers.

Validation: 78 passed, 10 native-Windows skips across five focused files.
Reverting only this production correction reproduces four late-sync
failures; restoring it returns the same suite to green.
2026-09-13 18:20:25 -04:00

86 lines
4.4 KiB
Python

"""All source selection routes hand off once, without old-process maintenance."""
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from hermes_cli import main, main_web_build, update_cmd, update_cmd_zip, update_cmd_maint
from tests.compat.old_updater_support import fresh_child, no_external_work # noqa: F401
@pytest.mark.parametrize("hook,args,kwargs", [
(update_cmd._prepare_updated_checkout, ("unused",), {"desktop": False}),
(update_cmd._reload_config_modules, (), {}),
(update_cmd._reload_process_scan_modules, (), {}),
(update_cmd._run_pending_fleet_restart, (), {}),
(main_web_build._run_with_idle_timeout, (["unused"], "unused"), {"idle_timeout_seconds": 10}),
(main_web_build._run_npm_install_deterministic, ("unused", "unused"), {"extra_args": ("arg",)}),
(main_web_build._nixos_build_env, (), {}),
(main._reexec_dependency_sync_off_windows_shim, (), {}),
(update_cmd_maint._print_update_summary, (), {
"node_failures": [], "desktop_build_ok": True, "pre_update_version": None}),
(update_cmd_maint._print_update_summary, (), {
"node_failures": ["dashboard"], "desktop_build_ok": False, "pre_update_version": "0.20.1"}),
(update_cmd_maint._finish_dashboard_update_cleanup, ([],), {}),
(update_cmd_maint._finish_dashboard_update_cleanup, (["dashboard"],), {
"already_restarted_units": {"hermes-serve"}}),
])
def test_historical_completion_hook_never_reports_success(hook, args, kwargs, fresh_child, capsys):
if hook in {update_cmd._prepare_updated_checkout, update_cmd._reload_config_modules,
update_cmd._reload_process_scan_modules, update_cmd._run_pending_fleet_restart}:
with pytest.raises(SystemExit) as error:
hook(*args, **kwargs)
assert error.value.code == 1
assert fresh_child.requests == []
assert 'run `hermes update` again' in capsys.readouterr().err
return
with fresh_child.exits():
hook(*args, **kwargs)
def test_incomplete_handoff_requires_explicit_update_retry(tmp_path, monkeypatch, capsys):
from hermes_cli import main
monkeypatch.setattr(main, "PROJECT_ROOT", tmp_path)
completion = Mock()
monkeypatch.setattr(update_cmd, "run_completion", completion)
with pytest.raises(SystemExit) as error:
update_cmd._complete_source_update(None)
assert error.value.code == 1
completion.assert_not_called()
output = capsys.readouterr()
assert "run `hermes update` again" in output.err
assert "Update complete" not in output.out
assert not (tmp_path / ".update-incomplete").exists()
assert not (tmp_path / ".lazy-refresh-incomplete").exists()
@pytest.mark.parametrize("route", ["pulled", "current", "zip"])
def test_every_route_hands_off_once(route, tmp_path, monkeypatch):
request = {"branch": "main", "receipt": {"update_id": "c" * 32}}
handed_off = []
monkeypatch.setattr(update_cmd, "_complete_source_update", handed_off.append, raising=False)
monkeypatch.setattr(update_cmd, "_m", lambda: SimpleNamespace(
PROJECT_ROOT=tmp_path, _resolve_update_branch=lambda args: "main"))
monkeypatch.setattr(update_cmd, "_verify_head_after_pull", lambda *a, **kw: "new-sha")
monkeypatch.setattr(update_cmd, "_prepare_updated_checkout", lambda *a, **kw: pytest.fail("old-process preparation"))
monkeypatch.setattr(update_cmd, "_write_fleet_restart_pending_marker", lambda **kw: None)
monkeypatch.setattr(update_cmd, "_sweep_bytecode_after_update", lambda *a: None)
monkeypatch.setattr(update_cmd_zip, "_abort_zip_update_if_dirty_tree", lambda: None)
swap = Mock()
monkeypatch.setattr(update_cmd_zip, "_download_and_swap_zip", swap)
plan = update_cmd._CheckoutPlan(
in_place_update=False, auto_stash_ref=None, parked_branch_switched=False,
upstream_checked=True, commit_count=1, prompt_for_restore=False, switch_block_reason=None)
if route == "pulled":
update_cmd._apply_pulled_update(
["git"], "main", "old-sha", plan, _windows_gateway_resume=None,
completion_request=request)
elif route == "current":
update_cmd._finish_already_up_to_date(
["git"], "main", "main", plan, gw_input_fn=None, completion_request=request)
else:
assert update_cmd_zip._update_via_zip(SimpleNamespace(), completion_request=request) is True
swap.assert_called_once()
assert handed_off == [request]