diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-08 12:55:55 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-08 12:55:55 -0500 |
| commit | e8eff25043c12a7d30dc347be60ce516a5afd05d (patch) | |
| tree | b4533792b350d6fd15d5e0144c452aa762ee43c6 /tests | |
| parent | da29e92aea51803234d25b6473b4d7fb82dbf7da (diff) | |
| download | archsetup-e8eff25043c12a7d30dc347be60ce516a5afd05d.tar.gz archsetup-e8eff25043c12a7d30dc347be60ce516a5afd05d.zip | |
fix(guard): allow same-version reinstalls through the live-update hook
The hypr-live-update-guard hook blocked on target names alone, so a same-version reinstall of hyprland (the maintenance console's integrity REINSTALL, right after a full update) aborted identically to a real upgrade. A pure reinstall writes identical bytes over identical bytes and can't crash the live compositor: the SIGABRT hazard needs the on-disk version to actually change.
The guard now compares each target's installed version (pacman -Q) against the sync-db candidate (expac -S %v). Both are read-only queries and safe under the transaction lock, pinned live with db.lck present. Targets that match pass as reinstalls, and the block message lists only the version-changing packages. Unresolvable versions (AUR targets, -U transactions, expac missing) still block conservatively.
Verified live on ratio: the previously blocked six-package reinstall ran clean with Hyprland up. The block path is pinned by the test suite (13 tests, four new). I deployed the script to /usr/local/bin on both hosts by hand since the installer only copies it on fresh systems.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/hypr-live-update-guard/test_hypr_live_update_guard.py | 53 |
1 files changed, 52 insertions, 1 deletions
diff --git a/tests/hypr-live-update-guard/test_hypr_live_update_guard.py b/tests/hypr-live-update-guard/test_hypr_live_update_guard.py index 5ec5ce8..578edc7 100644 --- a/tests/hypr-live-update-guard/test_hypr_live_update_guard.py +++ b/tests/hypr-live-update-guard/test_hypr_live_update_guard.py @@ -12,6 +12,10 @@ Test seams (env vars the production script honors): HYPR_GUARD_RUNNING 1/0 forces the Hyprland-running check (default: pgrep) HYPR_ALLOW_LIVE_UPDATE 1 overrides the guard (proceed anyway) HYPR_GUARD_SENTINEL path whose existence also overrides the guard + HYPR_GUARD_VERSIONS "pkg installed candidate" lines replacing the + pacman -Q / expac -S version lookups; when set, + a package absent from the map reads as unknown + (conservative block) Run from repo root: python3 -m unittest tests.hypr-live-update-guard.test_hypr_live_update_guard @@ -26,8 +30,18 @@ import unittest REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) GUARD = os.path.join(REPO_ROOT, "scripts", "hypr-live-update-guard") +# Every package the legacy tests feed, mapped to a version CHANGE — those +# tests describe real upgrades, and the map keeps them hermetic (no +# pacman/expac calls against the test host). +CHANGING_VERSIONS = "\n".join(( + "mesa 25.1.0-1 26.0.0-1", + "hyprland 0.55.3-1 0.55.4-1", + "vulkan-radeon 25.1.0-1 26.0.0-1", +)) -def run_guard(stdin="mesa\n", running="1", allow=None, sentinel=None): + +def run_guard(stdin="mesa\n", running="1", allow=None, sentinel=None, + versions=CHANGING_VERSIONS): env = dict(os.environ) env["HYPR_GUARD_RUNNING"] = running if allow is not None: @@ -35,6 +49,7 @@ def run_guard(stdin="mesa\n", running="1", allow=None, sentinel=None): # Point the sentinel at a path that does not exist unless a test sets one, # so the host's real /run state can't leak into the result. env["HYPR_GUARD_SENTINEL"] = sentinel if sentinel else "/nonexistent/guard-sentinel" + env["HYPR_GUARD_VERSIONS"] = versions return subprocess.run( ["sh", GUARD], input=stdin, capture_output=True, text=True, timeout=10, env=env, @@ -75,6 +90,42 @@ class HyprLiveUpdateGuard(unittest.TestCase): r = run_guard(stdin="", running="1") self.assertEqual(r.returncode, 1) + # --- Version awareness ------------------------------------------------ + + def test_same_version_reinstall_allows(self): + # a pure reinstall replaces identical bytes with identical bytes — + # no live-swap hazard, the guard must let it through + r = run_guard(stdin="hyprland\n", running="1", + versions="hyprland 0.55.4-1 0.55.4-1") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertEqual(r.stderr.strip(), "") + + def test_all_same_version_multi_pkg_allows(self): + versions = "\n".join(("mesa 26.1.4-1 26.1.4-1", + "hyprland 0.55.4-1 0.55.4-1", + "vulkan-radeon 26.1.4-1 26.1.4-1")) + r = run_guard(stdin="mesa\nhyprland\nvulkan-radeon\n", running="1", + versions=versions) + self.assertEqual(r.returncode, 0, r.stderr) + + def test_mixed_blocks_naming_only_the_version_changing(self): + versions = "\n".join(("mesa 25.1.0-1 26.0.0-1", + "hyprland 0.55.4-1 0.55.4-1")) + r = run_guard(stdin="mesa\nhyprland\n", running="1", + versions=versions) + self.assertEqual(r.returncode, 1) + self.assertIn("- mesa", r.stderr) + # the prose mentions the Hyprland session, so assert on the + # package-list line format, not the bare word + self.assertNotIn("- hyprland", r.stderr) + + def test_unknown_versions_block_conservatively(self): + # seam set but package absent from the map = the lookup failed + # (AUR target, -U transaction, expac missing) — stay safe + r = run_guard(stdin="mesa\n", running="1", versions="") + self.assertEqual(r.returncode, 1) + self.assertIn("- mesa", r.stderr) + # --- Override / error cases ----------------------------------------- def test_env_override_proceeds_even_when_running(self): |
