test(v2): define protocol and clean-break contracts

This commit is contained in:
2026-07-27 18:29:43 +02:00
parent ae98f383d6
commit 33b0c21292
2 changed files with 130 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
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)