Files
hermes-agent/tests/hermes_cli/test_boot_bootstrap.py
ethernet 59624b7ace tests: replace every bare host skipif with platforms()
The OS lanes are marker-driven: list_os_marked_tests.py picks the files
a lane imports from their platforms() specs and the lane selects with
-m platforms. A test gated with skipif(sys.platform != "win32") is
therefore never imported on the Windows lane and skipped everywhere else
— it runs on no host. skipif(sys.platform == "win32") tests were merely
invisible to the lane bookkeeping, but the rule the tree now follows is
one host marker, never a bare skipif.

Mechanical mapping, semantics preserved: skip-on-Windows → "posix",
skip-off-Windows → "windows", skip-off-Linux → "linux", skip-on-macOS →
"not macos". The former skip reasons stay as trailing comments. A
non-host condition (os.geteuid() == 0) stays a separate skipif beside
the marker, spelled getattr(os, "geteuid", ...) so the decorator still
imports on Windows.

Where the conversion would stack two platforms() marks on one test (the
conftest rejects that at collection) the narrower mark wins:
- test_update_wedged_gateway: the class is already platforms("linux");
  its per-test "needs UNIX sockets" marks were redundant and are gone.
- test_process_registry.TestSystemdCgroupIsolation: the class-level
  skip-on-Windows moves onto the 11 methods that had no host mark; the
  11 platforms("linux") methods keep theirs.
- test_file_ops_single_roundtrip: the two fifo tests drop their
  platforms("linux") in favour of the module's "posix" (mkfifo exists on
  macOS; both tests already skip when it does not).
- test_linux_desktop_entry / test_gateway_job_teardown_live: duplicate
  or wider marks removed.
2026-09-21 19:18:15 -04:00

293 lines
9.9 KiB
Python

"""Boot-time post-update bootstrap: identity, records, locks, single-flight.
The record files are an optimization layer over idempotent steps; these
tests assert the contracts that keep that safe: identity resolution from
real git trees and stamps, record scoping (per-install AND per-home), and the lock protocol including the double-check under lock.
"""
import json
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
import pytest
from hermes_cli import boot_bootstrap
from hermes_cli.boot_bootstrap import (
_RecordLock,
current_install_identity,
needs_bootstrap,
read_git_head,
read_last_known,
record_path,
run_boot_bootstrap,
_write_record,
)
def _git(args, cwd):
env = dict(os.environ)
env.update({
"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t",
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t",
"GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_SYSTEM": os.devnull,
})
return subprocess.run(
["git", *args], cwd=cwd, env=env, capture_output=True, text=True, check=True
)
@pytest.fixture
def repo(tmp_path):
root = tmp_path / "repo"
root.mkdir()
_git(["init", "-b", "main"], root)
(root / "f.txt").write_text("1", encoding="utf-8")
_git(["add", "."], root)
_git(["commit", "-m", "one"], root)
return root
def _head_sha(root):
return _git(["rev-parse", "HEAD"], root).stdout.strip()
# ── read_git_head ────────────────────────────────────────────────────
def test_git_selection_uses_pm_public_package_reader(repo, monkeypatch):
from types import SimpleNamespace
import pm
command = shutil.which("git")
assert command is not None
binary = Path(command)
requests = []
def installed(name):
requests.append(name)
return SimpleNamespace(binary=binary)
monkeypatch.setattr(pm, "installed_package", installed)
assert read_git_head(repo) == _head_sha(repo)
assert requests == ["git"]
def test_read_git_head_detached(repo):
sha = _head_sha(repo)
_git(["checkout", "--detach", sha], repo)
assert read_git_head(repo) == sha
def test_read_git_head_packed_refs(repo):
sha = _head_sha(repo)
_git(["pack-refs", "--all"], repo)
# Loose ref is gone; only packed-refs carries the branch now.
assert not (repo / ".git" / "refs" / "heads" / "main").exists()
assert read_git_head(repo) == sha
def test_read_git_head_worktree_gitfile(repo, tmp_path):
wt = tmp_path / "wt"
_git(["worktree", "add", str(wt)], repo)
assert (wt / ".git").is_file() # gitfile pointer, not a directory
assert read_git_head(wt) == _head_sha(wt)
def test_read_git_head_reftable(tmp_path):
"""The repo format that kills hand-rolled .git parsers.
A reftable repo stores refs in neither loose files nor packed-refs,
and its HEAD is a decoy (``ref: refs/heads/.invalid``) kept only so
pre-reftable tools fail loudly instead of misreading. Asking git
answers. If git here is too old for reftable, nothing to test.
"""
root = tmp_path / "rt"
root.mkdir()
try:
_git(["init", "--ref-format=reftable", "-b", "main", "."], root)
except subprocess.CalledProcessError:
pytest.skip("git too old for --ref-format=reftable")
(root / "f.txt").write_text("1", encoding="utf-8")
_git(["add", "."], root)
_git(["commit", "-m", "one"], root)
head = (root / ".git" / "HEAD").read_text(encoding="utf-8")
assert ".invalid" in head, "reftable decoy HEAD is the point of this test"
assert read_git_head(root) == _head_sha(root)
def test_read_git_head_missing_and_garbage(tmp_path):
assert read_git_head(tmp_path) is None
(tmp_path / ".git").write_text("not a gitdir pointer", encoding="utf-8")
assert read_git_head(tmp_path) is None
# ── current_install_identity ─────────────────────────────────────────
def test_identity_broken_tree_is_none(tmp_path):
assert current_install_identity(tmp_path) is None
(tmp_path / "install-stamp.json").write_text("garbage", encoding="utf-8")
assert current_install_identity(tmp_path) is None
# ── record paths ─────────────────────────────────────────────────────
def test_record_paths_key_on_install_root(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
a = record_path(tmp_path / "install-a")
b = record_path(tmp_path / "install-b")
assert a != b
# The key is a FOLDER (installs/<SHA16>/bootstrap/<profile>.json),
# not a filename suffix: same grandparent tree, different key dirs.
assert a.parent != b.parent
assert a.parent.parent.parent == b.parent.parent.parent # installs/
assert a.name == b.name # the profile filename is the shared part
@pytest.mark.platforms("posix") # requires symlink privilege on Windows
def test_symlinked_root_canonicalizes(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
real = tmp_path / "real-install"
real.mkdir()
link = tmp_path / "link-install"
link.symlink_to(real)
assert record_path(real) == record_path(link)
# ── needs_bootstrap ──────────────────────────────────────────────────
def test_needs_bootstrap_broken_tree_never_fires(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
assert needs_bootstrap(tmp_path / "nope") is None
assert run_boot_bootstrap(tmp_path / "nope") == {"home": "skipped"}
assert not record_path(tmp_path / "nope").exists()
# ── lock protocol ────────────────────────────────────────────────────
def test_lock_loser_skips(tmp_path):
record = tmp_path / "r.json"
first = _RecordLock(record)
second = _RecordLock(record)
assert first.acquire()
assert not second.acquire()
first.release()
assert second.acquire()
second.release()
def test_stale_lock_is_broken(tmp_path):
record = tmp_path / "r.json"
lock_path = record.with_name(record.name + ".lock")
lock_path.parent.mkdir(parents=True, exist_ok=True)
lock_path.write_text(
json.dumps({"pid": 1, "startedAt": time.time() - 3600}), encoding="utf-8"
)
lock = _RecordLock(record)
assert lock.acquire()
lock.release()
def test_fresh_lock_is_respected(tmp_path):
record = tmp_path / "r.json"
lock_path = record.with_name(record.name + ".lock")
lock_path.parent.mkdir(parents=True, exist_ok=True)
lock_path.write_text(
json.dumps({"pid": os.getpid(), "startedAt": time.time()}), encoding="utf-8"
)
assert not _RecordLock(record).acquire()
# ── run_boot_bootstrap ───────────────────────────────────────────────
@pytest.fixture
def fake_steps(monkeypatch):
calls = {"home": 0}
def home_step():
calls["home"] += 1
return {"ok": True}
from hermes_cli import post_update
monkeypatch.setattr(post_update, "BOOT_HOME_STEPS", (("h", home_step),))
return calls
def test_double_check_under_lock(repo, tmp_path, monkeypatch, fake_steps):
"""A racer that finished between our read and our acquire wins."""
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
sha = _head_sha(repo)
real_acquire = _RecordLock.acquire
def acquire_after_racer_finished(self):
got = real_acquire(self)
if got and self.path.name.endswith(".json.lock"):
# Simulate the previous holder completing just before us.
_write_record(record_path(repo), sha, {})
return got
monkeypatch.setattr(_RecordLock, "acquire", acquire_after_racer_finished)
result = run_boot_bootstrap(repo)
assert result["home"] == "done-by-other"
assert fake_steps["home"] == 0
def test_maybe_run_never_raises(monkeypatch, tmp_path):
monkeypatch.setattr(
boot_bootstrap, "run_boot_bootstrap",
lambda root: (_ for _ in ()).throw(RuntimeError("boom")),
)
boot_bootstrap.maybe_run_boot_bootstrap(tmp_path) # must not raise
def test_sealed_tree_bootstrap_end_to_end(tmp_path, monkeypatch):
"""The desktop-bundle-swap scenario. A sealed tree (install-stamp.json,
no .git) must bootstrap on first boot, no-op on the second, and RE-RUN
when the stamp's commit changes — that is the only signal a bundle
swap emits."""
import json as _json
from hermes_cli import post_update
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
sealed = tmp_path / "payload"
sealed.mkdir()
(sealed / "install-stamp.json").write_text(
_json.dumps({"commit": "aaaa1111", "payload": "full", "updateMechanism": "electron-updater"})
)
calls = {"n": 0}
def count():
calls["n"] += 1
return {"ok": True}
monkeypatch.setattr(post_update, "BOOT_HOME_STEPS", (("h", count),))
assert run_boot_bootstrap(sealed)["home"] != "skipped"
assert calls["n"] == 1
assert run_boot_bootstrap(sealed)["home"] == "skipped"
assert calls["n"] == 1 # second boot: identity unchanged, no work
# The bundle swap: same root, new stamp commit.
(sealed / "install-stamp.json").write_text(
_json.dumps({"commit": "bbbb2222", "payload": "full", "updateMechanism": "electron-updater"})
)
assert run_boot_bootstrap(sealed)["home"] != "skipped"
assert calls["n"] == 2, "a swapped bundle must re-run the bootstrap"