Files
Nomarchy/tools/check-compose-lock.py
Bernardo Magri e01303851d
All checks were successful
Check / eval (push) Successful in 3m6s
test(checks): pure-contract guards batch (#49)
Four new no-VM checks.* + CI py_compile expansion:
- hardware-db-modules: installer DB module names ∈ pinned
  nixos-hardware.nixosModules (a lock bump can rename a module and break
  profiled installs on just the matching DMI, invisible to any VM).
- installer-compose-lock: offline lock-composer contract on fixtures.
- installer-disko: pure Nix assert — swapSize "0"/"0G" → no @swap, "2G"
  → sized, withLuks wraps root (permanently guards the #46 install fix).
- windowrule-syntax: builds the generated hyprland.conf and fails on the
  pre-0.53 grammar / windowrulev2 keyword (guards ed7fd93).
CI py_compile now covers all tracked *.py; docs/TESTING.md §1 synced.

Implemented by a worktree agent; diff reviewed. Verified V0 (flake check)
+ V1 (each checks.x86_64-linux.* built on main, exit 0); agent's negative
tests confirm each guard fails on a regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:10:45 +01:00

99 lines
4.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""compose-lock.py contract check (flake check `installer-compose-lock`).
Usage: check-compose-lock.py <compose-lock.py>
Exercises the offline lock-composer the installer runs on the target with
no network: given nomarchy's own flake.lock and the baked locked/original
node metadata, it must emit a well-formed downstream lock where
root ─▶ nomarchy (locked to the baked rev+narHash)
└─▶ every upstream node, verbatim, one level down
and where `follows` paths (list-valued inputs, absolute from the old root)
are rebased under the new `nomarchy` node. Also pins the fail-closed
guards: a locked node without a narHash, and an upstream lock that already
carries a `nomarchy` node, must both abort.
"""
import json
import subprocess
import sys
import tempfile
from pathlib import Path
tool = sys.argv[1]
def run(src_lock, locked, original):
"""Run compose-lock on a fixture; return (returncode, stderr, out_json|None)."""
with tempfile.TemporaryDirectory() as d:
src = Path(d) / "upstream.lock"
out = Path(d) / "out.lock"
src.write_text(json.dumps(src_lock))
p = subprocess.run(
[sys.executable, tool, str(src), str(out),
json.dumps(locked), json.dumps(original)],
capture_output=True, text=True,
)
got = json.loads(out.read_text()) if p.returncode == 0 and out.exists() else None
return p.returncode, p.stderr, got
def check(cond, msg):
if not cond:
sys.exit(f"compose-lock contract: {msg}")
# A representative upstream lock: a plain input, and one that `follows`
# it (list-valued input — the case the rebasing logic exists for).
upstream = {
"nodes": {
"root": {"inputs": {"nixpkgs": "nixpkgs", "home-manager": "home-manager"}},
"nixpkgs": {"locked": {"type": "github", "narHash": "sha256-NP"}},
"home-manager": {
"inputs": {"nixpkgs": ["nixpkgs"]},
"locked": {"type": "github", "narHash": "sha256-HM"},
},
},
"root": "root",
"version": 7,
}
locked = {"type": "path", "path": "/nix/store/x", "narHash": "sha256-SELF", "lastModified": 1}
original = {"type": "git", "url": "https://example.invalid/nomarchy.git", "ref": "v1"}
rc, err, got = run(upstream, locked, original)
check(rc == 0, f"valid inputs were rejected (rc={rc}): {err}")
check(got is not None, "no output lock written on the happy path")
check(got["version"] == 7 and got["root"] == "root", "output is not a v7 lock rooted at 'root'")
check(got["nodes"]["root"] == {"inputs": {"nomarchy": "nomarchy"}},
f"root node must point only at nomarchy, got {got['nodes']['root']}")
nomarchy = got["nodes"].get("nomarchy")
check(nomarchy is not None, "no 'nomarchy' node in output")
check(nomarchy["locked"] == locked, "nomarchy node lost its baked locked metadata")
check(nomarchy["original"] == original, "nomarchy node lost its baked original metadata")
check(nomarchy["inputs"] == upstream["nodes"]["root"]["inputs"],
"nomarchy node did not inherit the upstream root's input set")
check(got["nodes"]["nixpkgs"] == upstream["nodes"]["nixpkgs"],
"a plain upstream node was mutated")
check(got["nodes"]["home-manager"]["inputs"]["nixpkgs"] == ["nomarchy", "nixpkgs"],
"a follows path was not rebased under the nomarchy node "
f"(got {got['nodes']['home-manager']['inputs']['nixpkgs']})")
# Fail-closed: no narHash in the baked locked metadata.
rc, err, _ = run(upstream, {"type": "path", "path": "/x"}, original)
check(rc != 0, "a locked node without a narHash was accepted")
check("narHash" in err, f"missing-narHash abort did not mention narHash: {err}")
# Fail-closed: an upstream lock that already names a nomarchy input.
poisoned = json.loads(json.dumps(upstream))
poisoned["nodes"]["nomarchy"] = {"locked": {"narHash": "sha256-X"}}
rc, err, _ = run(poisoned, locked, original)
check(rc != 0, "an upstream lock already carrying a 'nomarchy' node was accepted")
print("compose-lock contract: happy-path shape, follows rebasing, and both "
"fail-closed guards hold")