from __future__ import annotations import json from pathlib import Path from typing import Any import jsonschema import pytest ROOT = Path(__file__).parents[2] CONTRACT = ROOT / "contracts" / "repository" / "v1" FIXTURES = CONTRACT / "fixtures" def load(path: Path) -> Any: try: return json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as error: raise AssertionError(f"invalid contract JSON: {path}") from error def test_repository_and_manifest_golden_files_validate() -> None: for name in ("repository", "manifest"): schema = load(CONTRACT / f"{name}.schema.json") fixture = load(FIXTURES / f"valid-{name}.json") jsonschema.Draft202012Validator.check_schema(schema) jsonschema.validate(fixture, schema) def test_invalid_golden_files_are_rejected() -> None: for name in ("repository", "manifest"): schema = load(CONTRACT / f"{name}.schema.json") fixture = load(FIXTURES / f"invalid-{name}.json") with pytest.raises(jsonschema.ValidationError): jsonschema.validate(fixture, schema) def test_valid_golden_files_are_canonical_json() -> None: for path in sorted(FIXTURES.glob("valid-*.json")): data = load(path) canonical = json.dumps(data, sort_keys=True, separators=(",", ":")) + "\n" assert path.read_text(encoding="utf-8") == canonical def test_protocol_contracts_are_versioned_and_closed() -> None: repository_schema = load(CONTRACT / "repository.schema.json") manifest_schema = load(CONTRACT / "manifest.schema.json") assert repository_schema["properties"]["format_version"]["const"] == 1 assert manifest_schema["properties"]["format_version"]["const"] == 1 assert not repository_schema["additionalProperties"] assert not manifest_schema["additionalProperties"] def test_normalized_path_vectors_are_relative_posix_paths() -> None: vectors = load(CONTRACT / "normalized-paths.json") valid = [case for case in vectors if case["valid"]] invalid = [case for case in vectors if not case["valid"]] assert {case["raw"] for case in invalid} >= {"/absolute", "../escape", "a\\b"} for case in valid: normalized = case["normalized"] assert normalized and not normalized.startswith("/") assert "\\" not in normalized assert ".." not in normalized.split("/") def test_state_errors_capabilities_and_fault_points_are_frozen() -> None: transitions = load(CONTRACT / "execution-transitions.json") assert transitions["queued"] == ["cancelled", "preparing"] assert transitions["verifying"] == ["committed", "failed"] assert transitions["committed"] == [] errors = load(CONTRACT / "error-codes.json") assert len(errors) == len(set(errors)) assert all(code == code.lower() and " " not in code for code in errors) capabilities = load(CONTRACT / "capabilities-v2.0.json") assert capabilities["sources"] == ["local", "ssh"] assert not capabilities["features"]["tar_download"] assert not capabilities["features"]["postgresql"] assert not capabilities["features"]["mysql"] fault_points = load(CONTRACT / "fault-points.json") assert { "blob.before_write", "blob.after_fsync", "manifest.before_publish", "metadata.before_commit", "restore.before_replace", }.issubset(fault_points)