"""Canonical ID contract tests — pins the sha256: prefix format for all content-addressed IDs. Every content-addressed ID in the Muse ecosystem uses: ``sha256:<64 lowercase hex chars>`` This covers object_ids, snapshot_ids, and commit_ids. Random/opaque IDs (repo_id, collaborator IDs, etc.) are excluded from this contract. Tiers: 1. Hash functions return canonical form (synchronous) 2. WireCommit validation (synchronous) 3. WireSnapshot validation (synchronous) 4. Wire push endpoint rejects non-canonical IDs (async, requires DB) """ from __future__ import annotations import re import sys import uuid from datetime import datetime, timezone from pathlib import Path import msgpack import pytest from httpx import AsyncClient from pydantic import ValidationError from sqlalchemy.ext.asyncio import AsyncSession from musehub.muse_cli.snapshot import compute_snapshot_id, compute_commit_id from musehub.models.wire import WireCommit, WireSnapshot # Cross-verify against the Muse CLI implementation. sys.path.insert(0, str(Path.home() / "ecosystem" / "muse")) from muse.core.snapshot import compute_snapshot_id as cli_compute_snapshot_id from muse.core.snapshot import compute_commit_id as cli_compute_commit_id from tests.factories import create_repo # ── Module-level canonical ID regex ─────────────────────────────────────────── _CANONICAL_ID_RE = re.compile(r"^sha256:[0-9a-f]{64}$") # ── Shared test inputs (deterministic) ──────────────────────────────────────── _MANIFEST: dict[str, str] = { "README.md": "sha256:" + "a" * 64, "src/main.py": "sha256:" + "b" * 64, } _DIRS: list[str] = ["src"] _MESSAGE = "feat: add canonical ID tests" _TIMESTAMP = "2026-01-01T00:00:00+00:00" _PARENT_IDS: list[str] = [] # Pre-computed IDs used across tiers. _SNAP_ID_HUB = compute_snapshot_id(_MANIFEST, _DIRS) _COMMIT_ID_HUB = compute_commit_id(_PARENT_IDS, _SNAP_ID_HUB, _MESSAGE, _TIMESTAMP) # A valid canonical ID — a real sha256 hex wrapped in the prefix. import hashlib as _hashlib _VALID_SNAP_ID = "sha256:" + _hashlib.sha256(b"snapshot-fixture").hexdigest() _VALID_COMMIT_ID = "sha256:" + _hashlib.sha256(b"commit-fixture").hexdigest() _VALID_PARENT_ID = "sha256:" + _hashlib.sha256(b"parent-fixture").hexdigest() _VALID_OBJECT_ID = "sha256:" + _hashlib.sha256(b"object-fixture").hexdigest() # Bare hex (no prefix) — always invalid in the canonical contract. _BARE_HEX_64 = _hashlib.sha256(b"bare").hexdigest() # 64-char hex, no prefix assert len(_BARE_HEX_64) == 64 # UUID string — also invalid. _UUID_ID = str(uuid.uuid4()) # ── Helpers ─────────────────────────────────────────────────────────────────── def _utc_now() -> str: return datetime.now(tz=timezone.utc).isoformat() def _mp(data: object) -> bytes: return msgpack.packb(data, use_bin_type=True) def _make_push_payload( repo_id: str, *, commit_id: str = _VALID_COMMIT_ID, snapshot_id: str = _VALID_SNAP_ID, ) -> dict: """Build a minimal push payload with controllable commit_id and snapshot_id.""" return { "bundle": { "commits": [ { "commit_id": commit_id, "repo_id": repo_id, "branch": "main", "snapshot_id": snapshot_id, "message": "test: canonical ID push", "committed_at": _utc_now(), "parent_commit_id": None, "author": "Test User ", "sem_ver_bump": "none", } ], "snapshots": [ { "snapshot_id": snapshot_id, "manifest": {}, "created_at": _utc_now(), } ], "objects": [], }, "branch": "main", "force": False, "local_head": commit_id, } # ── Tier 1 — Hash functions return canonical form ───────────────────────────── class TestHashFunctionsReturnCanonicalForm: """compute_snapshot_id and compute_commit_id must return sha256:<64-hex> strings.""" def test_compute_snapshot_id_hub_returns_canonical(self) -> None: result = compute_snapshot_id(_MANIFEST, _DIRS) assert _CANONICAL_ID_RE.match(result), ( f"compute_snapshot_id (hub) returned {result!r}, " f"expected sha256:<64 lowercase hex chars>" ) def test_compute_commit_id_hub_returns_canonical(self) -> None: snap_id = compute_snapshot_id(_MANIFEST, _DIRS) result = compute_commit_id(_PARENT_IDS, snap_id, _MESSAGE, _TIMESTAMP) assert _CANONICAL_ID_RE.match(result), ( f"compute_commit_id (hub) returned {result!r}, " f"expected sha256:<64 lowercase hex chars>" ) def test_compute_snapshot_id_cli_returns_canonical(self) -> None: result = cli_compute_snapshot_id(_MANIFEST, _DIRS) assert _CANONICAL_ID_RE.match(result), ( f"compute_snapshot_id (cli) returned {result!r}, " f"expected sha256:<64 lowercase hex chars>" ) def test_compute_commit_id_cli_returns_canonical(self) -> None: snap_id = cli_compute_snapshot_id(_MANIFEST, _DIRS) result = cli_compute_commit_id(_PARENT_IDS, snap_id, _MESSAGE, _TIMESTAMP) assert _CANONICAL_ID_RE.match(result), ( f"compute_commit_id (cli) returned {result!r}, " f"expected sha256:<64 lowercase hex chars>" ) def test_hub_and_cli_snapshot_id_agree(self) -> None: """Both implementations must produce identical output for the same manifest.""" hub_result = compute_snapshot_id(_MANIFEST, _DIRS) cli_result = cli_compute_snapshot_id(_MANIFEST, _DIRS) assert hub_result == cli_result, ( f"snapshot_id mismatch: hub={hub_result!r}, cli={cli_result!r}" ) def test_hub_and_cli_commit_id_agree(self) -> None: """Both implementations must produce identical output for the same inputs.""" hub_snap = compute_snapshot_id(_MANIFEST, _DIRS) cli_snap = cli_compute_snapshot_id(_MANIFEST, _DIRS) # snapshot_ids must match first (tested separately), use hub value as input. hub_commit = compute_commit_id(_PARENT_IDS, hub_snap, _MESSAGE, _TIMESTAMP) cli_commit = cli_compute_commit_id(_PARENT_IDS, cli_snap, _MESSAGE, _TIMESTAMP) assert hub_commit == cli_commit, ( f"commit_id mismatch: hub={hub_commit!r}, cli={cli_commit!r}" ) def test_snapshot_id_is_deterministic(self) -> None: """Same inputs always produce the same snapshot_id.""" a = compute_snapshot_id(_MANIFEST, _DIRS) b = compute_snapshot_id(_MANIFEST, _DIRS) assert a == b def test_commit_id_is_deterministic(self) -> None: """Same inputs always produce the same commit_id.""" snap = compute_snapshot_id(_MANIFEST, _DIRS) a = compute_commit_id(_PARENT_IDS, snap, _MESSAGE, _TIMESTAMP) b = compute_commit_id(_PARENT_IDS, snap, _MESSAGE, _TIMESTAMP) assert a == b def test_snapshot_id_without_dirs_returns_canonical(self) -> None: """Omitting the directories argument still returns canonical form.""" result = compute_snapshot_id(_MANIFEST) assert _CANONICAL_ID_RE.match(result), ( f"compute_snapshot_id (no dirs) returned {result!r}" ) def test_snapshot_id_empty_manifest_returns_canonical(self) -> None: """An empty manifest produces a canonical ID (not an error).""" result = compute_snapshot_id({}) assert _CANONICAL_ID_RE.match(result), ( f"compute_snapshot_id (empty manifest) returned {result!r}" ) # ── Tier 2 — WireCommit validation ──────────────────────────────────────────── class TestWireCommitValidation: """WireCommit must enforce sha256: prefix on commit_id, snapshot_id, and parent_commit_id.""" def test_canonical_commit_id_accepted(self) -> None: commit = WireCommit( commit_id=_VALID_COMMIT_ID, snapshot_id=_VALID_SNAP_ID, ) assert commit.commit_id == _VALID_COMMIT_ID def test_bare_hex_commit_id_rejected(self) -> None: """A 64-char hex commit_id without the sha256: prefix must be rejected.""" with pytest.raises((ValueError, ValidationError)): WireCommit( commit_id=_BARE_HEX_64, snapshot_id=_VALID_SNAP_ID, ) def test_uuid_commit_id_rejected(self) -> None: """A UUID commit_id is not a content-addressed ID and must be rejected.""" with pytest.raises((ValueError, ValidationError)): WireCommit( commit_id=_UUID_ID, snapshot_id=_VALID_SNAP_ID, ) def test_bare_hex_snapshot_id_rejected(self) -> None: """WireCommit.snapshot_id must also carry the sha256: prefix.""" with pytest.raises((ValueError, ValidationError)): WireCommit( commit_id=_VALID_COMMIT_ID, snapshot_id=_BARE_HEX_64, ) def test_canonical_parent_commit_id_accepted(self) -> None: """A canonical parent_commit_id is valid.""" commit = WireCommit( commit_id=_VALID_COMMIT_ID, snapshot_id=_VALID_SNAP_ID, parent_commit_id=_VALID_PARENT_ID, ) assert commit.parent_commit_id == _VALID_PARENT_ID def test_none_parent_commit_id_accepted(self) -> None: """parent_commit_id=None is valid (root commit).""" commit = WireCommit( commit_id=_VALID_COMMIT_ID, snapshot_id=_VALID_SNAP_ID, parent_commit_id=None, ) assert commit.parent_commit_id is None def test_bare_hex_parent_commit_id_rejected(self) -> None: """parent_commit_id when non-None must carry the sha256: prefix.""" with pytest.raises((ValueError, ValidationError)): WireCommit( commit_id=_VALID_COMMIT_ID, snapshot_id=_VALID_SNAP_ID, parent_commit_id=_BARE_HEX_64, ) # ── Tier 3 — WireSnapshot validation ────────────────────────────────────────── class TestWireSnapshotValidation: """WireSnapshot must enforce sha256: prefix on snapshot_id.""" def test_canonical_snapshot_id_accepted(self) -> None: snap = WireSnapshot(snapshot_id=_VALID_SNAP_ID) assert snap.snapshot_id == _VALID_SNAP_ID def test_bare_hex_snapshot_id_rejected(self) -> None: """A 64-char hex snapshot_id without the sha256: prefix must be rejected.""" with pytest.raises((ValueError, ValidationError)): WireSnapshot(snapshot_id=_BARE_HEX_64) def test_uuid_snapshot_id_rejected(self) -> None: """A UUID snapshot_id is not a content-addressed ID and must be rejected.""" with pytest.raises((ValueError, ValidationError)): WireSnapshot(snapshot_id=_UUID_ID) def test_canonical_snapshot_with_manifest_accepted(self) -> None: snap = WireSnapshot( snapshot_id=_VALID_SNAP_ID, manifest={"README.md": _VALID_OBJECT_ID}, ) assert snap.snapshot_id == _VALID_SNAP_ID assert "README.md" in snap.manifest # ── Tier 4 — Wire push endpoint rejects non-canonical IDs ───────────────────── @pytest.mark.asyncio async def test_push_with_canonical_ids_returns_200( client: AsyncClient, db_session: AsyncSession, wire_headers: dict[str, str], ) -> None: """A push with fully canonical sha256: IDs throughout must succeed (200).""" repo = await create_repo(db_session, slug="canonical-ids-ok", owner="test-user-wire") payload = _make_push_payload( repo.repo_id, commit_id=_VALID_COMMIT_ID, snapshot_id=_VALID_SNAP_ID, ) resp = await client.post( f"/{repo.owner}/{repo.slug}/push", content=_mp(payload), headers=wire_headers, ) assert resp.status_code == 200, ( f"Expected 200 for canonical IDs but got {resp.status_code}: {resp.text}" ) @pytest.mark.asyncio async def test_push_with_bare_hex_commit_id_returns_422( client: AsyncClient, db_session: AsyncSession, wire_headers: dict[str, str], ) -> None: """A push where commit_id is bare hex (no sha256: prefix) must return 422.""" repo = await create_repo(db_session, slug="bare-hex-commit-422", owner="test-user-wire") payload = _make_push_payload( repo.repo_id, commit_id=_BARE_HEX_64, snapshot_id=_VALID_SNAP_ID, ) resp = await client.post( f"/{repo.owner}/{repo.slug}/push", content=_mp(payload), headers=wire_headers, ) assert resp.status_code == 422, ( f"Expected 422 for bare-hex commit_id but got {resp.status_code}: {resp.text}" ) @pytest.mark.asyncio async def test_push_with_bare_hex_snapshot_id_returns_422( client: AsyncClient, db_session: AsyncSession, wire_headers: dict[str, str], ) -> None: """A push where snapshot_id is bare hex (no sha256: prefix) must return 422.""" repo = await create_repo(db_session, slug="bare-hex-snap-422", owner="test-user-wire") payload = _make_push_payload( repo.repo_id, commit_id=_VALID_COMMIT_ID, snapshot_id=_BARE_HEX_64, ) resp = await client.post( f"/{repo.owner}/{repo.slug}/push", content=_mp(payload), headers=wire_headers, ) assert resp.status_code == 422, ( f"Expected 422 for bare-hex snapshot_id but got {resp.status_code}: {resp.text}" )