401efb0b2b
hermes_cli/update_cmd.py 11217 -> 10204 LOC; _cmd_update_impl 2797 -> 521 LOC. Decomposition (behavior-neutral, AST free-name verified) — _cmd_update_impl is now a thin orchestrator calling, in order: _clear_windows_venv_holders_or_exit -> _prepare_checkout_for_update (-> _CheckoutPlan) -> _repair_current_checkout | _pull_updates -> _sync_python_dependencies_after_pull -> _run_post_update_maintenance -> _restart_gateway_fleet_after_update (-> _GatewayRestartOutcome; _restart_systemd_gateway_units) -> _resume_windows_gateways_and_merge_outcome -> _verify_fleet_after_update. _resolve_manage_cmd hoisted to module level. Unified helpers: _git_run (49 captured git subprocess.run sites), _systemctl / _systemctl_reset_and_restart (17 sites + 2 pairs), _sweep_bytecode_after_update (3x), _print_bundled_skills_sync_report (ZIP+git), _ensure_venv_pip (ZIP+git), _self_and_non_gateway_ancestor_pids (2x), _record_update_step (4x), _write_gateway_update_exit_code for the 3 inline ".update_exit_code" writes. Dropped duplicate nested copies of _wait_for_service_active/_service_restart_sec/_print_items, dead upstream_exists, a dead if/pass branch and unused imports. Comments/docstrings hand-compacted (236 blocks, AST-identical with docstrings normalized); rationale/invariant sentences kept. Tests: source-inspection guards repointed to the helper that now owns the code (test_update_self_lock, test_update_fleet_check_fail_closed, test_update_apply_shallow_count); new regression test drives the real Windows resume/merge helper.
117 lines
4.7 KiB
Python
117 lines
4.7 KiB
Python
"""Shallow-checkout guard on the `hermes update` apply path (#53479).
|
|
|
|
`rev-list --count HEAD..origin/<branch>` on a shallow install can enumerate
|
|
the entire remote ancestry ("Found 9980 new commit(s)" on a depth-1 clone).
|
|
The apply path now detects shallow state, recovers the real count via the
|
|
GitHub compare API, and reports count-free wording when that fails —
|
|
mirroring the check path fixed in PR #86257.
|
|
|
|
These tests exercise the real _cmd_update_impl decision block by faking only
|
|
the subprocess layer (git) and the compare API — the count/print logic runs
|
|
for real.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
import hermes_cli.update_cmd as update_cmd
|
|
|
|
SHA_A = "a" * 40
|
|
SHA_B = "b" * 40
|
|
|
|
|
|
def _git_responder(*, shallow: bool, count: str):
|
|
"""Answer the git subprocess calls the count block makes."""
|
|
|
|
def fake_run(cmd, **kwargs):
|
|
joined = " ".join(cmd)
|
|
if "rev-list" in joined and "--count" in joined:
|
|
return MagicMock(returncode=0, stdout=f"{count}\n", stderr="")
|
|
if "--is-shallow-repository" in joined:
|
|
return MagicMock(returncode=0, stdout=("true\n" if shallow else "false\n"), stderr="")
|
|
if "rev-parse HEAD" in joined:
|
|
return MagicMock(returncode=0, stdout=f"{SHA_A}\n", stderr="")
|
|
if "rev-parse origin/main" in joined:
|
|
return MagicMock(returncode=0, stdout=f"{SHA_B}\n", stderr="")
|
|
return MagicMock(returncode=0, stdout="", stderr="")
|
|
|
|
return fake_run
|
|
|
|
|
|
def _run_count_block(*, shallow: bool, raw_count: str, api_count):
|
|
"""Execute exactly the apply-path count block with a faked git layer."""
|
|
import subprocess as real_subprocess
|
|
|
|
fake = _git_responder(shallow=shallow, count=raw_count)
|
|
with patch.object(update_cmd, "subprocess") as sub:
|
|
sub.run = MagicMock(side_effect=fake)
|
|
sub.CalledProcessError = real_subprocess.CalledProcessError
|
|
with patch("hermes_cli.banner._github_compare_behind", return_value=api_count):
|
|
# Reproduce the block's logic against the real module state.
|
|
git_cmd = ["git"]
|
|
result = sub.run(
|
|
git_cmd + ["rev-list", "HEAD..origin/main", "--count"],
|
|
capture_output=True, text=True, check=True,
|
|
)
|
|
commit_count = int(result.stdout.strip())
|
|
apply_is_shallow = (
|
|
sub.run(
|
|
git_cmd + ["rev-parse", "--is-shallow-repository"],
|
|
capture_output=True, text=True,
|
|
).stdout.strip()
|
|
== "true"
|
|
)
|
|
if commit_count > 0 and apply_is_shallow:
|
|
from hermes_cli.banner import _github_compare_behind
|
|
|
|
head_sha = sub.run(git_cmd + ["rev-parse", "HEAD"], capture_output=True, text=True).stdout.strip()
|
|
target_sha = sub.run(
|
|
git_cmd + ["rev-parse", "origin/main"], capture_output=True, text=True
|
|
).stdout.strip()
|
|
counted = _github_compare_behind(head_sha, target_sha)
|
|
commit_count = counted if counted is not None else -1
|
|
return commit_count
|
|
|
|
|
|
def test_source_matches_exercised_logic():
|
|
"""Guard: the block tested above must still exist in _cmd_update_impl.
|
|
|
|
If the apply path's shallow-count recovery is refactored away, this fails
|
|
and the mirrored logic in _run_count_block must be updated with it.
|
|
"""
|
|
import inspect
|
|
|
|
src = inspect.getsource(update_cmd._prepare_checkout_for_update)
|
|
assert "apply_is_shallow" in src
|
|
assert "_github_compare_behind" in src
|
|
# The "count unknown" print stays in _cmd_update_impl, which consumes the plan.
|
|
assert "commit count unknown on this shallow checkout" in inspect.getsource(
|
|
update_cmd._cmd_update_impl
|
|
)
|
|
|
|
|
|
def test_full_clone_keeps_exact_count():
|
|
assert _run_count_block(shallow=False, raw_count="7", api_count=None) == 7
|
|
|
|
|
|
def test_shallow_bogus_count_recovers_via_compare_api():
|
|
"""FAIL-BEFORE: reported the bogus 9980 as 'Found 9980 new commit(s)'."""
|
|
assert _run_count_block(shallow=True, raw_count="9980", api_count=12) == 12
|
|
|
|
|
|
def test_shallow_bogus_count_offline_reports_unknown():
|
|
assert _run_count_block(shallow=True, raw_count="9980", api_count=None) == -1
|
|
|
|
|
|
def test_shallow_local_ahead_treated_as_up_to_date():
|
|
assert _run_count_block(shallow=True, raw_count="3", api_count=0) == 0
|
|
|
|
|
|
def test_shallow_zero_count_short_circuits_without_api():
|
|
with patch("hermes_cli.banner._github_compare_behind") as api:
|
|
got = _run_count_block(shallow=True, raw_count="0", api_count=None)
|
|
# The block only consults the API when count > 0; a 0 count is trustworthy
|
|
# (HEAD == origin tip counts 0 even on shallow graphs).
|
|
assert got == 0
|