| 1 | #!/usr/bin/env python3 |
| 2 | """Build frozen old/new store probes and exercise disposable rollback databases.""" |
| 3 | import argparse |
| 4 | import io |
| 5 | import json |
| 6 | import pathlib |
| 7 | import subprocess |
| 8 | import sqlite3 |
| 9 | import tarfile |
| 10 | |
| 11 | parser = argparse.ArgumentParser() |
| 12 | parser.add_argument("--artifacts", required=True) |
| 13 | args = parser.parse_args() |
| 14 | repo = pathlib.Path(__file__).resolve().parents[1] |
| 15 | out = pathlib.Path(args.artifacts).resolve() |
| 16 | out.mkdir(parents=True, exist_ok=False) |
| 17 | current = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip() |
| 18 | versions = {"old876": "87662f5", "old8a": "8a126727", "old3ed": "3ed7665", "new": current} |
| 19 | fixtures = repo / "internal/docs/principalstore/testdata" |
| 20 | records = [] |
| 21 | for name, rev in versions.items(): |
| 22 | sha = subprocess.check_output(["git", "rev-parse", rev], cwd=repo, text=True).strip() |
| 23 | src = out / name |
| 24 | src.mkdir() |
| 25 | archive = subprocess.check_output(["git", "archive", sha], cwd=repo) |
| 26 | with tarfile.open(fileobj=io.BytesIO(archive)) as tree: |
| 27 | tree.extractall(src, filter="data") |
| 28 | cmd = src / "cmd/principal-compat" |
| 29 | cmd.mkdir() |
| 30 | (cmd / "main.go").write_bytes((fixtures / "compat_main.go.txt").read_bytes()) |
| 31 | step = "new" if name == "new" else "old" |
| 32 | (cmd / "step.go").write_bytes((fixtures / f"compat_{step}.go.txt").read_bytes()) |
| 33 | with (out / f"{name}-build.log").open("wb") as log: |
| 34 | subprocess.run(["go", "build", "-o", str(out / f"{name}-proof"), "./cmd/principal-compat"], cwd=src, stdout=log, stderr=log, check=True) |
| 35 | records.append({"name": name, "sha": sha}) |
| 36 | (out / "builds.json").write_text(json.dumps(records, indent=2)) |
| 37 | results = {} |
| 38 | for old in ["old876", "old8a", "old3ed"]: |
| 39 | db = out / f"{old}-cycle.db" |
| 40 | phases = [] |
| 41 | for index, binary in enumerate([old, "new", old, "new"]): |
| 42 | phase = f"{old}-{index}" |
| 43 | with (out / f"{phase}.stderr").open("wb") as log: |
| 44 | raw = subprocess.check_output([str(out / f"{binary}-proof"), "--db", str(db), "--address", f"synthetic-{index}@example.invalid", "--phase", phase], cwd=out / binary, stderr=log) |
| 45 | (out / f"{phase}.json").write_bytes(raw) |
| 46 | result = json.loads(raw) |
| 47 | if result["local_id"] != index + 1: |
| 48 | raise RuntimeError("old/new allocation or LastInsertId changed") |
| 49 | if index and any(result[k] != phases[0][k] for k in ["owner", "grant_author", "sqid", "content_hash", "encodings", "text"]): |
| 50 | raise RuntimeError("owner/grant/content changed across rollback") |
| 51 | if binary == "new" and (result["home_id"] != 2**52 or result["home_owned"] != 1): |
| 52 | raise RuntimeError("Home identity/ownership changed") |
| 53 | if index == 2 and result["home_owned"] != 1: |
| 54 | raise RuntimeError("old binary lost Home ownership") |
| 55 | if index and result["principal_count"] != index + 2: |
| 56 | raise RuntimeError("legacy insert did not mirror the canonical ledger") |
| 57 | phases.append(result) |
| 58 | with sqlite3.connect(db) as check: |
| 59 | if check.execute("PRAGMA foreign_key_check").fetchall(): |
| 60 | raise RuntimeError("foreign key damage across rollback") |
| 61 | if check.execute("SELECT COUNT(*) FROM people WHERE email='' OR id>=4503599627370496").fetchone()[0]: |
| 62 | raise RuntimeError("Home-only identity leaked into legacy people") |
| 63 | results[old] = phases |
| 64 | (out / "verified.json").write_text(json.dumps({"candidate": current, "versions": records, "cycles": results}, indent=2)) |
| 65 | print(out / "verified.json") |
| 66 | |