Files
hermes-agent/tests/scripts/test_payload_digests.py
ethernet 5eec230f48 fix(bundle): record tool digests after signing transforms
Staging digests became stale after PE repair or payload signing.
Refresh all tool facts atomically while preserving their identity.
Windows refreshes after the batch signer. The macOS wrapper delegates
to the installed signer and refreshes after children, before the outer
app signature. Entitlements and file selection remain intact.

The real builder dispatcher caught the rejected factory shape. Tests
execute the installed signing walk with only codesign intercepted,
then compare the recorded bytes at the outer signing boundary.
Corrupt facts abort that boundary. Enabling batching fails the tests.

Verification: 80 Python tests passed with 3 host skips. 36 JavaScript
tests passed with 1 host skip. Lint, config typecheck and schema pass.
No real Apple/Azure signature, native package install or release run.
2026-09-09 23:35:59 -04:00

81 lines
3.1 KiB
Python

"""Packaged facts describe final tool bytes without changing their identity."""
import copy
import json
from pathlib import Path
import subprocess
import sys
from pm.lock import Facts, Lockfile
from pm.store import current_target, tree_digest
from scripts.bundles import payload
def _payload(tmp_path):
root = tmp_path / "payload"
store = root / "tools"
entries = {"python": "python", "uv": "uv"}
lock = Lockfile(tmp_path / "lock.json")
for name, entry in entries.items():
directory = store / entry
directory.mkdir(parents=True)
(directory / "tool").write_bytes(b"staging bytes")
lock.set_pin(name, "1.0", {current_target(): {
"url": "https://example.invalid/tool.zip", "sha256": "a" * 64,
}})
lock.save()
payload.record_tools(root, lock.path, current_target(), entries)
Facts(store / "facts.json").record_state("venv", "selection", ["web"])
return root
def _rehash(root, cwd):
return subprocess.run(
[sys.executable, str(Path(payload.__file__)), "rehash", str(root)],
cwd=cwd, capture_output=True, text=True, encoding="utf-8", timeout=60,
)
def test_rehash_records_all_changed_tools_and_preserves_identity(tmp_path):
root = _payload(tmp_path)
path = root / "tools" / "facts.json"
before = json.loads(path.read_text(encoding="utf-8"))
(root / "tools/python/tool").write_bytes(b"final python bytes")
(root / "tools/uv/tool").write_bytes(b"final uv bytes")
result = _rehash(root, tmp_path)
assert result.returncode == 0, result.stderr
after = json.loads(path.read_text(encoding="utf-8"))
for name in ("python", "uv"):
expected = dict(before["packages"][name])
expected["digest"] = tree_digest(root / "tools" / expected["entry"])
assert after["packages"][name] == expected
assert expected["digest"] != before["packages"][name]["digest"]
assert after["packages"]["venv"] == before["packages"]["venv"]
saved = path.read_bytes()
assert _rehash(root, tmp_path).returncode == 0
assert path.read_bytes() == saved
def test_invalid_tool_evidence_never_partially_rewrites_facts(tmp_path):
root = _payload(tmp_path)
path = root / "tools" / "facts.json"
before = json.loads(path.read_text(encoding="utf-8"))
(root / "tools/python/tool").write_bytes(b"final python bytes")
outside = tmp_path / "outside"
outside.mkdir()
(outside / "kept").write_bytes(b"foreign")
for entry in ("missing", "../outside", str(outside)):
invalid = copy.deepcopy(before)
invalid["packages"]["uv"]["entry"] = entry
path.write_text(json.dumps(invalid), encoding="utf-8")
saved = path.read_bytes()
assert _rehash(root, tmp_path).returncode != 0
assert path.read_bytes() == saved
assert (outside / "kept").read_bytes() == b"foreign"
for raw in (b"not JSON", b'{"schema":1,"packages":{}}'):
path.write_bytes(raw)
assert _rehash(root, tmp_path).returncode != 0
assert path.read_bytes() == raw
path.unlink()
assert _rehash(root, tmp_path).returncode != 0
assert not path.exists()