Files
hermes-agent/tests/hermes_cli/test_update_receipt_pm_embed.py

232 lines
8.9 KiB
Python

"""Tests: update receipts embed ONLY their own pm sync sections.
The updater embeds the sync receipt that carries ITS correlation id —
read from pm.receipt's per-context completions, never from latest.json.
A sync that finished before the update began, or one running in a
concurrent thread, must never be misattributed to the invoking update.
"""
from __future__ import annotations
import json
import threading
import pytest
import hermes_cli.update_receipt as ur
@pytest.fixture
def homed(tmp_path, monkeypatch):
import hermes_constants
monkeypatch.setattr(hermes_constants, "get_hermes_home", lambda: tmp_path)
import pm.receipt as pm_receipt_mod
monkeypatch.setattr(pm_receipt_mod, "_receipt_dir", lambda: tmp_path / "logs" / "update_receipts")
monkeypatch.setattr(ur, "_receipt_dir", lambda: tmp_path / "logs" / "update_receipts")
return tmp_path
@pytest.fixture(autouse=True)
def _isolated_receipt_state():
"""No leaked in-flight receipts, completion maps, or correlation
between tests."""
import pm.receipt as pm_receipt_mod
pm_receipt_mod._current.set(None)
pm_receipt_mod._completed_by_update.set(None)
ur._current.set(None)
yield
pm_receipt_mod._current.set(None)
pm_receipt_mod._completed_by_update.set(None)
ur._current.set(None)
def _sync_under_update(**sections):
"""A pm sync that runs WHILE the update receipt is open — the real
in-process update flow (sync_venv)."""
import pm.receipt as pm_receipt_mod
pm_receipt_mod.begin("sync")
if "venv_rebuild" in sections:
pm_receipt_mod.record_venv_rebuild(**sections.pop("venv_rebuild"))
if "features" in sections:
pm_receipt_mod.record_feature_list(sections.pop("features"))
if "warnings" in sections:
for message in sections.pop("warnings"):
pm_receipt_mod.record_warning(message)
if "refusal" in sections:
pm_receipt_mod.record_refusal(*sections.pop("refusal"))
pm_receipt_mod.finalize("ok")
def test_update_receipt_embeds_pm_sections(homed):
ur.begin_update_receipt()
_sync_under_update(
venv_rebuild={"ok": True, "reason": ""},
features=["web", "acp"],
)
ur.record_step("git-pull", True)
path = ur.finalize_update_receipt("success")
data = json.loads((homed / "logs" / "update_receipts" / "latest.json").read_text(encoding="utf-8"))
assert json.loads(path.read_text(encoding="utf-8")) == data
assert data["outcome"] == "success"
assert data["pm_venv_rebuild"] == {"ok": True, "reason": ""}
assert data["pm_feature_list"] == ["web", "acp"]
assert data["pm_sync_outcome"] == "ok"
assert data["update_id"]
def test_legacy_worker_receipt_fields_still_embed(homed):
from pm.receipt import accept_worker_receipt
ur.begin_update_receipt()
update_id = ur.current_correlation_id()
legacy = {"update_id": update_id, "outcome": "ok",
"plugin_bisect": [{"plugin": "old-plugin", "action": "disabled"}]}
accept_worker_receipt(legacy, update_id)
path = ur.finalize_update_receipt("success")
data = json.loads(path.read_text(encoding="utf-8"))
assert data["pm_plugin_bisect"] == legacy["plugin_bisect"]
def test_stale_sync_from_before_the_update_is_never_embedded(homed):
"""The audit defect: an old sync receipt on disk (latest.json's newest)
must not ride along on an update that caused no sync of its own."""
import pm.receipt as pm_receipt_mod
pm_receipt_mod.begin("sync")
pm_receipt_mod.record_venv_rebuild(True, "stale run")
pm_receipt_mod.finalize("ok")
assert pm_receipt_mod.latest()["venv_rebuild"]["reason"] == "stale run"
ur.begin_update_receipt()
path = ur.finalize_update_receipt("success")
data = json.loads(path.read_text(encoding="utf-8"))
assert "pm_venv_rebuild" not in data
assert "pm_sync_outcome" not in data
def test_concurrent_thread_sync_is_never_embedded(homed):
"""A sync finalizing in another thread (cadence plugin-check, parallel
ensure) finalizes into ITS context — the invoking update must not
embed it, even though it hit latest.json first."""
import pm.receipt as pm_receipt_mod
ur.begin_update_receipt()
seen: dict = {}
def concurrent_sync():
pm_receipt_mod.begin("sync")
pm_receipt_mod.record_venv_rebuild(True, "not my update")
seen["path"] = pm_receipt_mod.finalize("ok")
t = threading.Thread(target=concurrent_sync)
t.start()
t.join()
assert seen["path"] is not None # it did write a receipt...
path = ur.finalize_update_receipt("success")
data = json.loads(path.read_text(encoding="utf-8"))
assert "pm_venv_rebuild" not in data # ...but not into THIS update
def test_nested_update_ids_do_not_cross_embed(homed):
"""The steering-required scenario: outer update → outer sync → nested
update → nested sync → finalize inner → finalize outer. BOTH update
receipts finalize with a path, and each embeds ONLY its own sync —
the nested sync never displaces the outer update's."""
import pm.receipt as pm_receipt_mod
ur.begin_update_receipt()
outer_id = ur.current_correlation_id()
pm_receipt_mod.begin("sync")
pm_receipt_mod.record_venv_rebuild(True, "outer rebuild")
pm_receipt_mod.record_step("outer-sync", True)
pm_receipt_mod.finalize("ok")
ur.begin_update_receipt() # nested update — new id, outer preserved behind the token
inner_id = ur.current_correlation_id()
assert inner_id != outer_id
pm_receipt_mod.begin("sync")
pm_receipt_mod.record_venv_rebuild(True, "inner rebuild")
pm_receipt_mod.record_step("inner-sync", True)
pm_receipt_mod.finalize("ok")
inner_path = ur.finalize_update_receipt("success")
assert inner_path is not None
assert json.loads(inner_path.read_text(encoding="utf-8"))["pm_venv_rebuild"]["reason"] == "inner rebuild"
assert pm_receipt_mod.last_for_update(inner_id) is None
assert ur.current_correlation_id() == outer_id
assert pm_receipt_mod.last_for_update(outer_id)["steps"][0]["name"] == "outer-sync"
# outer finalize: the outer sync is STILL its own completion — the
# nested update's finalize restored the outer receipt and must not
# have dropped it or let the inner sync ride along.
outer_path = ur.finalize_update_receipt("partial")
assert outer_path is not None
outer_data = json.loads(outer_path.read_text(encoding="utf-8"))
assert outer_data["update_id"] == outer_id
assert outer_data["pm_venv_rebuild"]["reason"] == "outer rebuild"
assert "inner rebuild" not in json.dumps(outer_data)
assert outer_data["pm_steps"][0]["name"] == "outer-sync"
assert pm_receipt_mod.last_for_update(outer_id) is None
assert inner_path != outer_path
inner_data = json.loads(inner_path.read_text(encoding="utf-8"))
assert inner_data["update_id"] == inner_id
assert inner_data["pm_steps"][0]["name"] == "inner-sync"
assert inner_data["pm_venv_rebuild"]["reason"] == "inner rebuild"
latest = json.loads((homed / "logs/update_receipts/latest.json").read_text(encoding="utf-8"))
assert latest == outer_data
# outermost finalize popped the last receipt: no correlation remains
assert ur.current_correlation_id() is None
def test_sync_after_nested_update_finalizes_embeds_into_outer(homed):
"""Continuing the outer update after the nested one closed: a sync
begun under the restored outer id embeds into the outer receipt."""
import pm.receipt as pm_receipt_mod
ur.begin_update_receipt()
ur.begin_update_receipt()
ur.finalize_update_receipt("success")
pm_receipt_mod.begin("sync")
pm_receipt_mod.record_venv_rebuild(False, "already in sync")
pm_receipt_mod.finalize("ok")
path = ur.finalize_update_receipt("success")
data = json.loads(path.read_text(encoding="utf-8"))
assert data["pm_venv_rebuild"] == {"ok": False, "reason": "already in sync"}
def test_pm_warnings_and_refusal_embed(homed):
ur.begin_update_receipt()
_sync_under_update(
warnings=["shim quarantine skipped: file locked"],
refusal=("lazy-install", "extras outside the frozen feature set"),
)
path = ur.finalize_update_receipt("success")
data = json.loads(path.read_text(encoding="utf-8"))
assert data["pm_warnings"][0]["message"] == "shim quarantine skipped: file locked"
assert data["pm_refusal"]["code"] == "lazy-install"
assert data["pm_refusal"]["detail"] == "extras outside the frozen feature set"
def test_embed_failure_never_breaks_the_update_receipt(homed, monkeypatch):
def boom():
raise RuntimeError("pm import exploded")
import pm.receipt as pm_receipt_mod
monkeypatch.setattr(pm_receipt_mod, "last_for_update", boom)
ur.begin_update_receipt()
path = ur.finalize_update_receipt("success")
assert path is not None # receipt written despite the embed failure
data = json.loads(path.read_text(encoding="utf-8"))
assert data["outcome"] == "success"