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.
81 lines
3.1 KiB
Python
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()
|