fix(update): drop the ZIP swap owner lock file once settled; test the deferred fork push

(cherry picked from commit f45a0b340a060ea349ebb45b5cd6d98954a7900f)
(cherry picked from commit ded3fd987bf02311898515c7e4d4d857d23bf53b)
This commit is contained in:
Teknium
2026-10-03 10:13:27 -07:00
parent 247ee2eeb7
commit f2bfdb5d9e
2 changed files with 16 additions and 5 deletions

View File

@@ -440,12 +440,17 @@ _ZIP_STAGING_SUFFIX, _ZIP_OLD_SUFFIX = ".hermes-update-staging", ".hermes-update
@contextlib.contextmanager
def zip_swap_owner_lock(root: Path, *, wait: float = 0.0):
"""Yields True while this process owns the ZIP swap lock, False when a live owner holds it."""
"""Yields True while this process owns the ZIP swap lock, False when a live owner holds it.
The lock file is removed again once no journal is left, so a settled install carries no
breadcrumb (the swap itself also runs under the update lock; this one only proves liveness)."""
lock_path = Path(root) / (ZIP_SWAP_JOURNAL + ".lock")
try:
fd = os.open(Path(root) / (ZIP_SWAP_JOURNAL + ".lock"), os.O_RDWR | os.O_CREAT, 0o644)
fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o644)
except OSError:
yield True # unwritable root: no swap can run there either
return
owned = False
try:
deadline = time.monotonic() + wait
while not _lock_fd(fd, True):
@@ -453,12 +458,16 @@ def zip_swap_owner_lock(root: Path, *, wait: float = 0.0):
yield False
return
time.sleep(0.05)
owned = True
try:
yield True
finally:
_lock_fd(fd, False)
finally:
os.close(fd)
if owned and not (Path(root) / ZIP_SWAP_JOURNAL).exists():
with contextlib.suppress(OSError):
lock_path.unlink()
def write_zip_swap_journal(root: Path, phase: str, entries: list) -> None:

View File

@@ -149,9 +149,11 @@ def test_update_and_upstream_network_calls_disable_terminal_prompts(monkeypatch,
assert update_cmd_git._sync_with_upstream_if_needed(["git"], tmp_path, assume_yes=True)
from hermes_cli.update_custody import git_subcommand
# The fork's fast-forward is local (`merge --ff-only upstream/main` after the fetch): no
# credential helper ever runs under the checkout lock fd a mutator inherits (R2).
network = [(args, kwargs) for args, kwargs in calls if git_subcommand(args) != "rev-parse"]
# The fork push runs only after the update is validated (_push_synced_fork), never inside the sync.
update_cmd_git._sync_fork_with_upstream(["git"], tmp_path)
# The fork's fast-forward is local (`merge --ff-only` after the fetch): no credential helper ever
# runs under the checkout lock fd a mutator inherits (R2).
network = [(args, kwargs) for args, kwargs in calls if git_subcommand(args) in {"fetch", "pull", "push", "merge"}]
assert [git_subcommand(args) for args, _ in network] == ["fetch", "fetch", "merge", "push"]
assert network[2][0][-1] == upstream # the counted commit, not a name a local branch can shadow
for args, kwargs in network: