build: bind checkpoint manifest to acceptance

This commit is contained in:
Codex 2026-10-21 18:35:04 +02:00
parent 644c99a9ca
commit 3c26e1bdab
9 changed files with 191 additions and 22 deletions

View file

@ -21,7 +21,7 @@ The repository now provides:
- a checksummed acceptance-kit generator containing only allowlisted public - a checksummed acceptance-kit generator containing only allowlisted public
synthetic inputs, with provenance binding to the repository commit and synthetic inputs, with provenance binding to the repository commit and
non-executing artifact-verification report, including the Rayforge pin and non-executing artifact-verification report, including the Rayforge pin and
bundle, installer and addon-verification fields; bundle, installer, addon-verification and source checkpoint-manifest fields;
- a fail-closed manual desktop-acceptance report and validator; - a fail-closed manual desktop-acceptance report and validator;
- pinned upstream contract/license manifests and a drift checker; - pinned upstream contract/license manifests and a drift checker;
- a fail-closed API-23 adapter isolating Rayforge's internal driver registry; - a fail-closed API-23 adapter isolating Rayforge's internal driver registry;

View file

@ -57,6 +57,9 @@ Rayforge commit, requires the manifest and bundle to contain exactly the
versioned repository addon file set with matching digests, and requires the versioned repository addon file set with matching digests, and requires the
pinned Rayforge license both inside the ZIP and as a sidecar. Missing, changed pinned Rayforge license both inside the ZIP and as a sidecar. Missing, changed
or additional addon files fail verification. or additional addon files fail verification.
The schema-1 verification output records the exact checkpoint-manifest name
and SHA-256 plus the fixed checkpoint and addon identities, so later manual
acceptance can retain the complete artifact provenance chain.
It never executes the application or installer, and a passing result retains It never executes the application or installer, and a passing result retains
the manual GUI interaction gate. The installer, manifest and license sidecar the manual GUI interaction gate. The installer, manifest and license sidecar
form one distribution set. form one distribution set.
@ -90,7 +93,8 @@ report into the kit at creation time:
```powershell ```powershell
python scripts/prepare_m1_acceptance_kit.py ` python scripts/prepare_m1_acceptance_kit.py `
--artifact-verification .build/artifacts/rayforge-windows-verification.json --artifact-verification .build/artifacts/rayforge-windows-verification.json `
--checkpoint-manifest .build/artifacts/rayforge-windows-checkpoint.json
``` ```
The default output is `.build/acceptance-kit/`. The generator uses a fixed The default output is `.build/acceptance-kit/`. The generator uses a fixed
@ -102,7 +106,8 @@ artifact-verification report by SHA-256. Editing, replacing or referring to an
external provenance report therefore fails validation. The copied report must external provenance report therefore fails validation. The copied report must
also name the pinned Rayforge commit, contain valid bundle and installer also name the pinned Rayforge commit, contain valid bundle and installer
SHA-256 values, and confirm that the exact current addon file count was SHA-256 values, and confirm that the exact current addon file count was
verified. verified. Its own schema, checkpoint identity and addon identity are fixed, and
its recorded checkpoint-manifest hash must match the copied schema-2 manifest.
1. Pass the non-executing artifact verifier above. 1. Pass the non-executing artifact verifier above.
2. Launch the portable build with a fresh application-data directory. 2. Launch the portable build with a fresh application-data directory.

View file

@ -335,7 +335,8 @@ ignored build area, records their hashes and refuses to overwrite an existing
test run. Its schema-2 provenance chain binds the editable manual report to the test run. Its schema-2 provenance chain binds the editable manual report to the
exact kit manifest, repository commit and non-executing artifact-verification exact kit manifest, repository commit and non-executing artifact-verification
report; substituted or changed inputs, a different Rayforge pin, malformed report; substituted or changed inputs, a different Rayforge pin, malformed
artifact hashes or an unverified addon fail validation. artifact hashes, a changed checkpoint manifest or an unverified addon fail
validation.
The pinned upstream contracts and license notices are now machine-verifiable. The pinned upstream contracts and license notices are now machine-verifiable.
Rayforge's documented addon lifecycle is viable, but production driver Rayforge's documented addon lifecycle is viable, but production driver
registration still needs a supported symmetric registry API. The current registration still needs a supported symmetric registry API. The current

View file

@ -49,15 +49,28 @@ def create_kit(
output: Path, output: Path,
repository_commit: str | None = None, repository_commit: str | None = None,
artifact_verification: Path | None = None, artifact_verification: Path | None = None,
checkpoint_manifest: Path | None = None,
) -> dict: ) -> dict:
verification_source = None verification_source = None
if artifact_verification is not None: checkpoint_source = None
if (artifact_verification is None) != (checkpoint_manifest is None):
raise ValueError(
"artifact verification and checkpoint manifest must be supplied together"
)
if artifact_verification is not None and checkpoint_manifest is not None:
verification_source = artifact_verification.resolve() verification_source = artifact_verification.resolve()
checkpoint_source = checkpoint_manifest.resolve()
verification = json.loads( verification = json.loads(
verification_source.read_text(encoding="utf-8-sig") verification_source.read_text(encoding="utf-8-sig")
) )
checkpoint_bytes = checkpoint_source.read_bytes()
checkpoint = json.loads(checkpoint_bytes.decode("utf-8-sig"))
checkpoint_record = verification.get("checkpoint_manifest", {})
if ( if (
verification.get("status") != "passed" verification.get("schema_version") != 1
or verification.get("verification")
!= "Rayforge Windows no-device artifact verification"
or verification.get("status") != "passed"
or verification.get("hardware_access") is not False or verification.get("hardware_access") is not False
or verification.get("executed_binaries") is not False or verification.get("executed_binaries") is not False
or verification.get("manual_interaction_required") is not True or verification.get("manual_interaction_required") is not True
@ -65,6 +78,19 @@ def create_kit(
or SHA256.fullmatch(verification.get("bundle_sha256", "")) is None or SHA256.fullmatch(verification.get("bundle_sha256", "")) is None
or SHA256.fullmatch(verification.get("installer_sha256", "")) is None or SHA256.fullmatch(verification.get("installer_sha256", "")) is None
or verification.get("verified_addon_files") != EXPECTED_ADDON_FILES or verification.get("verified_addon_files") != EXPECTED_ADDON_FILES
or verification.get("addon") != "the_pirate_ray_jcz"
or checkpoint_record.get("path") != checkpoint_source.name
or checkpoint_record.get("sha256")
!= hashlib.sha256(checkpoint_bytes).hexdigest()
or checkpoint.get("schema_version") != 2
or checkpoint.get("checkpoint") != "Rayforge Windows no-device bundle"
or checkpoint.get("addon") != verification.get("addon")
or checkpoint.get("rayforge_commit")
!= verification.get("rayforge_commit")
or checkpoint.get("bundle", {}).get("sha256")
!= verification.get("bundle_sha256")
or checkpoint.get("installer", {}).get("sha256")
!= verification.get("installer_sha256")
): ):
raise ValueError("artifact-verification report did not pass the no-device gate") raise ValueError("artifact-verification report did not pass the no-device gate")
@ -116,6 +142,8 @@ def create_kit(
if verification_source is not None: if verification_source is not None:
verification_target = output / "artifact-verification.json" verification_target = output / "artifact-verification.json"
shutil.copyfile(verification_source, verification_target) shutil.copyfile(verification_source, verification_target)
checkpoint_target = output / checkpoint_source.name
shutil.copyfile(checkpoint_source, checkpoint_target)
report["artifact_verification"] = { report["artifact_verification"] = {
"status": "passed", "status": "passed",
"report_path": verification_target.name, "report_path": verification_target.name,
@ -136,13 +164,22 @@ def main() -> int:
type=Path, type=Path,
default=ROOT / ".build" / "acceptance-kit", default=ROOT / ".build" / "acceptance-kit",
) )
parser.add_argument(
"--checkpoint-manifest",
type=Path,
help="schema-2 manifest consumed by the artifact verifier",
)
parser.add_argument( parser.add_argument(
"--artifact-verification", "--artifact-verification",
type=Path, type=Path,
help="passing JSON output from verify_rayforge_windows_checkpoint.py", help="passing JSON output from verify_rayforge_windows_checkpoint.py",
) )
args = parser.parse_args() args = parser.parse_args()
result = create_kit(args.output, artifact_verification=args.artifact_verification) result = create_kit(
args.output,
artifact_verification=args.artifact_verification,
checkpoint_manifest=args.checkpoint_manifest,
)
print(f"kit={args.output.resolve()}") print(f"kit={args.output.resolve()}")
print(f"files={len(result['files'])}") print(f"files={len(result['files'])}")
print(f"repository_commit={result['repository_commit']}") print(f"repository_commit={result['repository_commit']}")

View file

@ -67,14 +67,24 @@ def _validate_evidence(report_root: Path, records: list[dict]) -> list[str]:
def _bound_json(report_root: Path, binding: dict, label: str) -> tuple[dict | None, list[str]]: def _bound_json(report_root: Path, binding: dict, label: str) -> tuple[dict | None, list[str]]:
value = binding.get("report_path") or binding.get("manifest_path") or "" value = (
binding.get("report_path")
or binding.get("manifest_path")
or binding.get("path")
or ""
)
relative = PurePosixPath(value) relative = PurePosixPath(value)
if not value or relative.is_absolute() or ".." in relative.parts: if not value or relative.is_absolute() or ".." in relative.parts:
return None, [f"unsafe {label} path: {value!r}"] return None, [f"unsafe {label} path: {value!r}"]
path = report_root.joinpath(*relative.parts) path = report_root.joinpath(*relative.parts)
if not path.is_file(): if not path.is_file():
return None, [f"{label} is missing: {value}"] return None, [f"{label} is missing: {value}"]
digest = binding.get("report_sha256") or binding.get("manifest_sha256") or "" digest = (
binding.get("report_sha256")
or binding.get("manifest_sha256")
or binding.get("sha256")
or ""
)
actual = hashlib.sha256(path.read_bytes()).hexdigest() actual = hashlib.sha256(path.read_bytes()).hexdigest()
if not SHA256.fullmatch(digest) or digest != actual: if not SHA256.fullmatch(digest) or digest != actual:
return None, [f"{label} SHA-256 mismatch: {value}"] return None, [f"{label} SHA-256 mismatch: {value}"]
@ -112,6 +122,13 @@ def _validate_artifact_report(report_root: Path, binding: dict) -> list[str]:
return errors return errors
if verification.get("status") != "passed": if verification.get("status") != "passed":
errors.append("artifact verification did not pass") errors.append("artifact verification did not pass")
if verification.get("schema_version") != 1:
errors.append("unsupported artifact-verification schema")
if (
verification.get("verification")
!= "Rayforge Windows no-device artifact verification"
):
errors.append("unexpected artifact-verification identity")
if verification.get("hardware_access") is not False: if verification.get("hardware_access") is not False:
errors.append("artifact verification does not preserve the hardware gate") errors.append("artifact verification does not preserve the hardware gate")
if verification.get("executed_binaries") is not False: if verification.get("executed_binaries") is not False:
@ -120,12 +137,37 @@ def _validate_artifact_report(report_root: Path, binding: dict) -> list[str]:
errors.append("artifact verification dropped the manual interaction gate") errors.append("artifact verification dropped the manual interaction gate")
if verification.get("rayforge_commit") != RAYFORGE_COMMIT: if verification.get("rayforge_commit") != RAYFORGE_COMMIT:
errors.append("artifact verification uses the wrong Rayforge commit") errors.append("artifact verification uses the wrong Rayforge commit")
if verification.get("addon") != "the_pirate_ray_jcz":
errors.append("artifact verification uses the wrong addon")
if SHA256.fullmatch(verification.get("bundle_sha256", "")) is None: if SHA256.fullmatch(verification.get("bundle_sha256", "")) is None:
errors.append("artifact verification has an invalid bundle SHA-256") errors.append("artifact verification has an invalid bundle SHA-256")
if SHA256.fullmatch(verification.get("installer_sha256", "")) is None: if SHA256.fullmatch(verification.get("installer_sha256", "")) is None:
errors.append("artifact verification has an invalid installer SHA-256") errors.append("artifact verification has an invalid installer SHA-256")
if verification.get("verified_addon_files") != EXPECTED_ADDON_FILES: if verification.get("verified_addon_files") != EXPECTED_ADDON_FILES:
errors.append("artifact verification did not verify the exact addon file set") errors.append("artifact verification did not verify the exact addon file set")
checkpoint, checkpoint_errors = _bound_json(
report_root,
verification.get("checkpoint_manifest", {}),
"checkpoint manifest",
)
errors.extend(checkpoint_errors)
if checkpoint is not None:
if checkpoint.get("schema_version") != 2:
errors.append("unsupported checkpoint-manifest schema")
if checkpoint.get("checkpoint") != "Rayforge Windows no-device bundle":
errors.append("unexpected checkpoint-manifest identity")
if checkpoint.get("addon") != verification.get("addon"):
errors.append("checkpoint-manifest addon mismatch")
if checkpoint.get("rayforge_commit") != verification.get("rayforge_commit"):
errors.append("checkpoint-manifest Rayforge commit mismatch")
if checkpoint.get("bundle", {}).get("sha256") != verification.get(
"bundle_sha256"
):
errors.append("checkpoint-manifest bundle SHA-256 mismatch")
if checkpoint.get("installer", {}).get("sha256") != verification.get(
"installer_sha256"
):
errors.append("checkpoint-manifest installer SHA-256 mismatch")
return errors return errors

View file

@ -65,9 +65,14 @@ def _relative_below(path: PurePosixPath, root: PurePosixPath) -> PurePosixPath |
def verify(manifest_path: Path) -> dict: def verify(manifest_path: Path) -> dict:
manifest_path = manifest_path.resolve() manifest_path = manifest_path.resolve()
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig")) manifest_bytes = manifest_path.read_bytes()
manifest = json.loads(manifest_bytes.decode("utf-8-sig"))
if manifest.get("schema_version") != 2: if manifest.get("schema_version") != 2:
raise ValueError("unsupported checkpoint schema") raise ValueError("unsupported checkpoint schema")
if manifest.get("checkpoint") != "Rayforge Windows no-device bundle":
raise ValueError("unexpected checkpoint identity")
if manifest.get("addon") != "the_pirate_ray_jcz":
raise ValueError("unexpected checkpoint addon")
if manifest.get("rayforge_commit") != UPSTREAM["commit"]: if manifest.get("rayforge_commit") != UPSTREAM["commit"]:
raise ValueError("Rayforge commit does not match the repository pin") raise ValueError("Rayforge commit does not match the repository pin")
if manifest.get("hardware_access") is not False or manifest.get("transport") is not None: if manifest.get("hardware_access") is not False or manifest.get("transport") is not None:
@ -143,8 +148,15 @@ def verify(manifest_path: Path) -> dict:
verified_addon_files = len(packaged_addon) verified_addon_files = len(packaged_addon)
return { return {
"schema_version": 1,
"verification": "Rayforge Windows no-device artifact verification",
"status": "passed", "status": "passed",
"rayforge_commit": manifest["rayforge_commit"], "rayforge_commit": manifest["rayforge_commit"],
"addon": manifest["addon"],
"checkpoint_manifest": {
"path": manifest_path.name,
"sha256": _digest(manifest_bytes),
},
"hardware_access": False, "hardware_access": False,
"executed_binaries": False, "executed_binaries": False,
"bundle_sha256": manifest["bundle"]["sha256"], "bundle_sha256": manifest["bundle"]["sha256"],

View file

@ -16,8 +16,20 @@ from scripts.prepare_m1_acceptance_kit import (
class M1AcceptanceKitTests(unittest.TestCase): class M1AcceptanceKitTests(unittest.TestCase):
@staticmethod @staticmethod
def _verification(status="passed"): def _artifact_pair(root: Path, status="passed"):
return { checkpoint = {
"schema_version": 2,
"checkpoint": "Rayforge Windows no-device bundle",
"addon": "the_pirate_ray_jcz",
"rayforge_commit": RAYFORGE_COMMIT,
"bundle": {"sha256": "1" * 64},
"installer": {"sha256": "2" * 64},
}
checkpoint_path = root / "rayforge-windows-checkpoint.json"
checkpoint_path.write_text(json.dumps(checkpoint), encoding="utf-8")
verification = {
"schema_version": 1,
"verification": "Rayforge Windows no-device artifact verification",
"status": status, "status": status,
"hardware_access": False, "hardware_access": False,
"executed_binaries": False, "executed_binaries": False,
@ -26,7 +38,15 @@ class M1AcceptanceKitTests(unittest.TestCase):
"bundle_sha256": "1" * 64, "bundle_sha256": "1" * 64,
"installer_sha256": "2" * 64, "installer_sha256": "2" * 64,
"verified_addon_files": EXPECTED_ADDON_FILES, "verified_addon_files": EXPECTED_ADDON_FILES,
"addon": "the_pirate_ray_jcz",
"checkpoint_manifest": {
"path": checkpoint_path.name,
"sha256": hashlib.sha256(checkpoint_path.read_bytes()).hexdigest(),
},
} }
verification_path = root / "verification.json"
verification_path.write_text(json.dumps(verification), encoding="utf-8")
return verification_path, checkpoint_path
def test_kit_contains_only_allowlisted_public_inputs(self): def test_kit_contains_only_allowlisted_public_inputs(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
@ -84,15 +104,13 @@ class M1AcceptanceKitTests(unittest.TestCase):
def test_passing_artifact_verification_is_bound_into_report(self): def test_passing_artifact_verification_is_bound_into_report(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory) root = Path(directory)
verification = root / "verification.json" verification, checkpoint = self._artifact_pair(root)
verification.write_text(
json.dumps(self._verification()), encoding="utf-8"
)
output = root / "kit" output = root / "kit"
create_kit( create_kit(
output, output,
repository_commit="e" * 40, repository_commit="e" * 40,
artifact_verification=verification, artifact_verification=verification,
checkpoint_manifest=checkpoint,
) )
copied = output / "artifact-verification.json" copied = output / "artifact-verification.json"
report = json.loads((output / "manual-report.json").read_text(encoding="utf-8")) report = json.loads((output / "manual-report.json").read_text(encoding="utf-8"))
@ -106,22 +124,20 @@ class M1AcceptanceKitTests(unittest.TestCase):
def test_failed_artifact_verification_is_rejected(self): def test_failed_artifact_verification_is_rejected(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory) root = Path(directory)
verification = root / "verification.json" verification, checkpoint = self._artifact_pair(root, status="failed")
verification.write_text(
json.dumps(self._verification(status="failed")), encoding="utf-8"
)
with self.assertRaisesRegex(ValueError, "did not pass"): with self.assertRaisesRegex(ValueError, "did not pass"):
create_kit( create_kit(
root / "kit", root / "kit",
repository_commit="f" * 40, repository_commit="f" * 40,
artifact_verification=verification, artifact_verification=verification,
checkpoint_manifest=checkpoint,
) )
def test_wrong_rayforge_pin_or_missing_hashes_are_rejected(self): def test_wrong_rayforge_pin_or_missing_hashes_are_rejected(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory) root = Path(directory)
verification = root / "verification.json" verification, checkpoint = self._artifact_pair(root)
result = self._verification() result = json.loads(verification.read_text(encoding="utf-8"))
result["rayforge_commit"] = "0" * 40 result["rayforge_commit"] = "0" * 40
result["bundle_sha256"] = "not-a-hash" result["bundle_sha256"] = "not-a-hash"
verification.write_text(json.dumps(result), encoding="utf-8") verification.write_text(json.dumps(result), encoding="utf-8")
@ -130,6 +146,20 @@ class M1AcceptanceKitTests(unittest.TestCase):
root / "kit", root / "kit",
repository_commit="f" * 40, repository_commit="f" * 40,
artifact_verification=verification, artifact_verification=verification,
checkpoint_manifest=checkpoint,
)
def test_changed_checkpoint_manifest_is_rejected(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory)
verification, checkpoint = self._artifact_pair(root)
checkpoint.write_text("{}", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "did not pass"):
create_kit(
root / "kit",
repository_commit="f" * 40,
artifact_verification=verification,
checkpoint_manifest=checkpoint,
) )

View file

@ -42,7 +42,19 @@ class M1ManualAcceptanceTests(unittest.TestCase):
"manifest_sha256": hashlib.sha256(manifest_path.read_bytes()).hexdigest(), "manifest_sha256": hashlib.sha256(manifest_path.read_bytes()).hexdigest(),
"repository_commit": "b" * 40, "repository_commit": "b" * 40,
} }
checkpoint = {
"schema_version": 2,
"checkpoint": "Rayforge Windows no-device bundle",
"addon": "the_pirate_ray_jcz",
"rayforge_commit": RAYFORGE_COMMIT,
"bundle": {"sha256": "1" * 64},
"installer": {"sha256": "2" * 64},
}
checkpoint_path = root / "rayforge-windows-checkpoint.json"
checkpoint_path.write_text(json.dumps(checkpoint), encoding="utf-8")
verification = { verification = {
"schema_version": 1,
"verification": "Rayforge Windows no-device artifact verification",
"status": "passed", "status": "passed",
"hardware_access": False, "hardware_access": False,
"executed_binaries": False, "executed_binaries": False,
@ -51,6 +63,11 @@ class M1ManualAcceptanceTests(unittest.TestCase):
"bundle_sha256": "1" * 64, "bundle_sha256": "1" * 64,
"installer_sha256": "2" * 64, "installer_sha256": "2" * 64,
"verified_addon_files": EXPECTED_ADDON_FILES, "verified_addon_files": EXPECTED_ADDON_FILES,
"addon": "the_pirate_ray_jcz",
"checkpoint_manifest": {
"path": checkpoint_path.name,
"sha256": hashlib.sha256(checkpoint_path.read_bytes()).hexdigest(),
},
} }
verification_path = root / "artifact-verification.json" verification_path = root / "artifact-verification.json"
verification_path.write_text(json.dumps(verification), encoding="utf-8") verification_path.write_text(json.dumps(verification), encoding="utf-8")
@ -163,6 +180,19 @@ class M1ManualAcceptanceTests(unittest.TestCase):
self.assertIn("artifact verification uses the wrong Rayforge commit", result["errors"]) self.assertIn("artifact verification uses the wrong Rayforge commit", result["errors"])
self.assertIn("artifact verification has an invalid installer SHA-256", result["errors"]) self.assertIn("artifact verification has an invalid installer SHA-256", result["errors"])
def test_changed_checkpoint_manifest_is_rejected(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory)
report = self._passing(root)
(root / "rayforge-windows-checkpoint.json").write_text(
"{}", encoding="utf-8"
)
result = validate(self._write(root, report))
self.assertEqual("failed", result["status"])
self.assertTrue(
any("checkpoint manifest SHA-256 mismatch" in error for error in result["errors"])
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -94,6 +94,9 @@ class WindowsCheckpointVerifierTests(unittest.TestCase):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
result = verify(self._checkpoint(Path(directory))) result = verify(self._checkpoint(Path(directory)))
self.assertEqual(result["status"], "passed") self.assertEqual(result["status"], "passed")
self.assertEqual(1, result["schema_version"])
self.assertEqual("the_pirate_ray_jcz", result["addon"])
self.assertRegex(result["checkpoint_manifest"]["sha256"], r"^[0-9a-f]{64}$")
self.assertFalse(result["executed_binaries"]) self.assertFalse(result["executed_binaries"])
expected_count = sum( expected_count = sum(
path.is_file() and "__pycache__" not in path.parts path.is_file() and "__pycache__" not in path.parts
@ -132,6 +135,15 @@ class WindowsCheckpointVerifierTests(unittest.TestCase):
with self.assertRaises(FileNotFoundError): with self.assertRaises(FileNotFoundError):
verify(manifest) verify(manifest)
def test_checkpoint_and_addon_identity_are_required(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
manifest = self._checkpoint(Path(directory))
data = json.loads(manifest.read_text(encoding="utf-8"))
data["addon"] = "foreign_addon"
manifest.write_text(json.dumps(data), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "unexpected checkpoint addon"):
verify(manifest)
def test_manifest_cannot_omit_or_change_repository_addon_source(self): def test_manifest_cannot_omit_or_change_repository_addon_source(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
manifest = self._checkpoint(Path(directory)) manifest = self._checkpoint(Path(directory))