from __future__ import annotations import json import subprocess import tempfile import unittest from pathlib import Path from scripts.lib import final_pack_contract REPO_ROOT = Path(__file__).resolve().parents[2] class FinalPackContractTests(unittest.TestCase): def commit_fixture(self, root: Path) -> str: subprocess.run(["git", "init", "-q", str(root)], check=True) subprocess.run( ["git", "-C", str(root), "config", "user.email", "contract@example.com"], check=True, ) subprocess.run( ["git", "-C", str(root), "config", "user.name", "Contract Test"], check=True, ) subprocess.run(["git", "-C", str(root), "add", "."], check=True) subprocess.run( ["git", "-C", str(root), "commit", "-qm", "fixture"], check=True, ) return subprocess.check_output( ["git", "-C", str(root), "rev-parse", "HEAD"], text=True, ).strip() def fixture(self) -> tuple[Path, Path]: temporary_directory = tempfile.TemporaryDirectory() self.addCleanup(temporary_directory.cleanup) root = Path(temporary_directory.name) intermediate = root / "packs/source" final = root / "packs/final" overlay = intermediate / "frontend" overlay.mkdir(parents=True) final.mkdir(parents=True) (overlay / "overlay.ts").write_text("export {};\n") (intermediate / "pack.json").write_text('{"name":"source"}\n') (intermediate / ".build-lineage.json").write_text('{"source":true}\n') (final / "pack.json").write_text('{"name":"final"}\n') (final / "objects.json").write_text('{"Account":{}}\n') stack_lock = { "pack_build": { "official_app_packs_repo": "pingcap-inc/nexuscrm-app-packs", "official_pack_path": "salesforce-crm-standard", "intermediate_pack_path": "packs/source", "final_pack_path": "packs/final", "flow_whitelist": ["Flow_B", "Flow_A"], } } stack_lock_path = root / "stack.lock.json" stack_lock_path.write_text(json.dumps(stack_lock) + "\n") lineage = { "stage": "final", "officialPack": { "repo": "pingcap-inc/nexuscrm-app-packs", "ref": "2" * 40, "packPath": "salesforce-crm-standard", }, "upstreamRuntime": { "repo": "pingcap-inc/NexusCRM", "ref": "1" * 40, }, "downstream": { "repo": "pingcap-inc/nexuscrm-pingcap-pack", "stackLockSha256": final_pack_contract.sha256_file(stack_lock_path), "intermediatePackPath": "packs/source", "intermediatePackManifestSha256": final_pack_contract.sha256_file( intermediate / "pack.json" ), "frontendOverlayPath": "packs/source/frontend", "frontendOverlayFileCount": 1, "intermediateBuildLineagePath": "packs/source/.build-lineage.json", "intermediateBuildLineageSha256": final_pack_contract.sha256_file( intermediate / ".build-lineage.json" ), }, "assembly": { "finalPackPath": "packs/final", "flowWhitelist": ["Flow_A", "Flow_B"], }, } (final / ".build-lineage.json").write_text(json.dumps(lineage) + "\n") return root, final def test_inspect_checkout_returns_exact_build_receipt(self) -> None: root, final = self.fixture() receipt = final_pack_contract.inspect_checkout(root) self.assertEqual(receipt.final_pack_root, final.resolve()) self.assertEqual(receipt.upstream_commit, "1" * 40) self.assertEqual(receipt.official_app_packs_commit, "2" * 40) self.assertEqual( receipt.metadata_tree_sha256, final_pack_contract.metadata_tree_sha256(final), ) def test_metadata_digest_excludes_lineage_but_includes_served_bytes(self) -> None: root, final = self.fixture() original = final_pack_contract.metadata_tree_sha256(final) (final / ".build-lineage.json").write_text('{"changed":true}\n') self.assertEqual(final_pack_contract.metadata_tree_sha256(final), original) (final / "objects.json").write_text('{"Account":{"changed":true}}\n') self.assertNotEqual(final_pack_contract.metadata_tree_sha256(final), original) def test_exact_git_digest_matches_commit_not_mutable_checkout(self) -> None: root, final = self.fixture() revision = self.commit_fixture(root) final_pack_path, exact_digest = ( final_pack_contract.final_pack_metadata_sha256_from_git(root, revision) ) self.assertEqual(final_pack_path, "packs/final") self.assertEqual(exact_digest, final_pack_contract.metadata_tree_sha256(final)) (final / "objects.json").write_text('{"Account":{"dirty":true}}\n') self.assertNotEqual(exact_digest, final_pack_contract.metadata_tree_sha256(final)) self.assertEqual( exact_digest, final_pack_contract.metadata_tree_sha256_from_git( root, revision, final_pack_path, ), ) def test_exact_git_digest_rejects_symlinked_metadata(self) -> None: root, final = self.fixture() (final / "linked.json").symlink_to("objects.json") revision = self.commit_fixture(root) with self.assertRaises(final_pack_contract.FinalPackContractError) as captured: final_pack_contract.final_pack_metadata_sha256_from_git(root, revision) self.assertIn( "metadata Git tree must not contain symlinks", captured.exception.errors, ) def test_exact_git_digest_excludes_ignored_checkout_bytes(self) -> None: root, final = self.fixture() ignored = final / "ignored.json" (root / ".gitignore").write_text("packs/final/ignored.json\n") revision = self.commit_fixture(root) ignored.write_text('{"ignored":true}\n') status = subprocess.check_output( ["git", "-C", str(root), "status", "--porcelain=v1", "--untracked-files=all"], text=True, ) self.assertEqual(status, "") _, exact_digest = final_pack_contract.final_pack_metadata_sha256_from_git( root, revision, ) self.assertNotEqual(exact_digest, final_pack_contract.metadata_tree_sha256(final)) def test_rejects_branch_refs_and_stale_intermediate_manifest(self) -> None: root, final = self.fixture() lineage_path = final / ".build-lineage.json" lineage = json.loads(lineage_path.read_text()) lineage["upstreamRuntime"]["ref"] = "main" lineage["downstream"]["intermediatePackManifestSha256"] = "0" * 64 lineage_path.write_text(json.dumps(lineage) + "\n") with self.assertRaises(final_pack_contract.FinalPackContractError) as captured: final_pack_contract.inspect_checkout(root) self.assertIn( "final pack lineage upstreamRuntime.ref must be an exact 40-character commit", captured.exception.errors, ) self.assertIn( "final pack lineage intermediatePackManifestSha256 does not match the intermediate pack", captured.exception.errors, ) def test_rejects_malformed_repository_without_duplicate_object_error(self) -> None: root, final = self.fixture() lineage_path = final / ".build-lineage.json" lineage = json.loads(lineage_path.read_text()) lineage["officialPack"]["repo"] = "owner/" lineage_path.write_text(json.dumps(lineage) + "\n") with self.assertRaises(final_pack_contract.FinalPackContractError) as captured: final_pack_contract.inspect_checkout(root) self.assertEqual( captured.exception.errors.count( "final pack lineage officialPack.repo must be a GitHub owner/repo value" ), 1, ) self.assertNotIn( "final pack lineage officialPack must be an object", captured.exception.errors, ) def test_rejects_overlay_drift_and_path_escape(self) -> None: root, final = self.fixture() lineage_path = final / ".build-lineage.json" lineage = json.loads(lineage_path.read_text()) lineage["downstream"]["frontendOverlayFileCount"] = 0 lineage_path.write_text(json.dumps(lineage) + "\n") with self.assertRaises(final_pack_contract.FinalPackContractError) as captured: final_pack_contract.inspect_checkout(root) self.assertTrue( any("frontendOverlayFileCount" in error for error in captured.exception.errors) ) stack_lock_path = root / "stack.lock.json" stack_lock = json.loads(stack_lock_path.read_text()) stack_lock["pack_build"]["final_pack_path"] = "../outside" stack_lock_path.write_text(json.dumps(stack_lock) + "\n") with self.assertRaises(final_pack_contract.FinalPackContractError) as captured: final_pack_contract.inspect_checkout(root) self.assertIn( "stack.lock.json pack_build.final_pack_path must stay inside the repository", captured.exception.errors, ) def test_repository_checkout_satisfies_contract(self) -> None: receipt = final_pack_contract.inspect_checkout(REPO_ROOT) self.assertEqual( receipt.final_pack_path, "packs/NOT-EDIT-final-merged-packs", ) self.assertRegex(receipt.metadata_tree_sha256, r"^sha256:[0-9a-f]{64}$") def test_legacy_tuple_contract_is_fully_retired(self) -> None: retired_paths = ( REPO_ROOT / "release" / "release-tuple.json", REPO_ROOT / "scripts" / ("validate_" + "release_tuple.py"), REPO_ROOT / "scripts" / ("test_validate_" + "release_tuple.py"), REPO_ROOT / "scripts" / ("verify_hubspot_recovery_" + "release_tuple.py"), REPO_ROOT / "scripts" / ("test_verify_hubspot_recovery_" + "release_tuple.py"), ) for path in retired_paths: self.assertFalse(path.exists(), path) active_contract_sources = ( "scripts/build_pingcap_salesforce_sample_pack.sh", "scripts/verify_final_pack_regen.py", "scripts/local_app_install.py", "deploy/online/runtime/common.py", "deployments/offline/runtime/common.py", "deployments/offline/runtime/reconcile/layout_bundle_phase0_source_inventory.py", "deployments/offline/runtime/reconcile/layout_bundle_phase0_live_inventory.py", "scripts/create_serviceification_candidate_receipt.py", ".github/workflows/serviceification-bom-gate.yml", ".agents/skills/edit-layout/SKILL.md", "CLAUDE.md", "docs/stack-test-model.md", ) retired_markers = ( "release/" + "release-tuple.json", "validate_" + "release_tuple", "--from-" + "release-tuple", "--no-stamp-" + "release-tuple", ) for relative in active_contract_sources: source = (REPO_ROOT / relative).read_text() for marker in retired_markers: self.assertNotIn(marker, source, relative) if __name__ == "__main__": unittest.main()