gabriel / musehub public
test_id_canonical.py python
362 lines 14.0 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 days ago
1 """Canonical ID contract tests — pins the sha256: prefix format for all content-addressed IDs.
2
3 Every content-addressed ID in the Muse ecosystem uses:
4 ``sha256:<64 lowercase hex chars>``
5
6 This covers object_ids, snapshot_ids, and commit_ids.
7 Random/opaque IDs (repo_id, collaborator IDs, etc.) are excluded from this contract.
8
9 Tiers:
10 1. Hash functions return canonical form (synchronous)
11 2. WireCommit validation (synchronous)
12 3. WireSnapshot validation (synchronous)
13 4. Wire push endpoint rejects non-canonical IDs (async, requires DB)
14 """
15 from __future__ import annotations
16
17 import re
18 import sys
19 import uuid
20 from datetime import datetime, timezone
21 from pathlib import Path
22
23 import msgpack
24 import pytest
25 from httpx import AsyncClient
26 from pydantic import ValidationError
27 from sqlalchemy.ext.asyncio import AsyncSession
28
29 from musehub.muse_cli.snapshot import compute_snapshot_id, compute_commit_id
30 from musehub.models.wire import WireCommit, WireSnapshot
31
32 # Cross-verify against the Muse CLI implementation.
33 sys.path.insert(0, str(Path.home() / "ecosystem" / "muse"))
34 from muse.core.snapshot import compute_snapshot_id as cli_compute_snapshot_id
35 from muse.core.snapshot import compute_commit_id as cli_compute_commit_id
36
37 from tests.factories import create_repo
38
39 # ── Module-level canonical ID regex ───────────────────────────────────────────
40
41 _CANONICAL_ID_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
42
43 # ── Shared test inputs (deterministic) ────────────────────────────────────────
44
45 _MANIFEST: dict[str, str] = {
46 "README.md": "sha256:" + "a" * 64,
47 "src/main.py": "sha256:" + "b" * 64,
48 }
49 _DIRS: list[str] = ["src"]
50 _MESSAGE = "feat: add canonical ID tests"
51 _TIMESTAMP = "2026-01-01T00:00:00+00:00"
52 _PARENT_IDS: list[str] = []
53
54 # Pre-computed IDs used across tiers.
55 _SNAP_ID_HUB = compute_snapshot_id(_MANIFEST, _DIRS)
56 _COMMIT_ID_HUB = compute_commit_id(_PARENT_IDS, _SNAP_ID_HUB, _MESSAGE, _TIMESTAMP)
57
58 # A valid canonical ID — a real sha256 hex wrapped in the prefix.
59 import hashlib as _hashlib
60 _VALID_SNAP_ID = "sha256:" + _hashlib.sha256(b"snapshot-fixture").hexdigest()
61 _VALID_COMMIT_ID = "sha256:" + _hashlib.sha256(b"commit-fixture").hexdigest()
62 _VALID_PARENT_ID = "sha256:" + _hashlib.sha256(b"parent-fixture").hexdigest()
63 _VALID_OBJECT_ID = "sha256:" + _hashlib.sha256(b"object-fixture").hexdigest()
64
65 # Bare hex (no prefix) — always invalid in the canonical contract.
66 _BARE_HEX_64 = _hashlib.sha256(b"bare").hexdigest() # 64-char hex, no prefix
67 assert len(_BARE_HEX_64) == 64
68
69 # UUID string — also invalid.
70 _UUID_ID = str(uuid.uuid4())
71
72
73 # ── Helpers ───────────────────────────────────────────────────────────────────
74
75 def _utc_now() -> str:
76 return datetime.now(tz=timezone.utc).isoformat()
77
78
79 def _mp(data: object) -> bytes:
80 return msgpack.packb(data, use_bin_type=True)
81
82
83 def _make_push_payload(
84 repo_id: str,
85 *,
86 commit_id: str = _VALID_COMMIT_ID,
87 snapshot_id: str = _VALID_SNAP_ID,
88 ) -> dict:
89 """Build a minimal push payload with controllable commit_id and snapshot_id."""
90 return {
91 "bundle": {
92 "commits": [
93 {
94 "commit_id": commit_id,
95 "repo_id": repo_id,
96 "branch": "main",
97 "snapshot_id": snapshot_id,
98 "message": "test: canonical ID push",
99 "committed_at": _utc_now(),
100 "parent_commit_id": None,
101 "author": "Test User <[email protected]>",
102 "sem_ver_bump": "none",
103 }
104 ],
105 "snapshots": [
106 {
107 "snapshot_id": snapshot_id,
108 "manifest": {},
109 "created_at": _utc_now(),
110 }
111 ],
112 "objects": [],
113 },
114 "branch": "main",
115 "force": False,
116 "local_head": commit_id,
117 }
118
119
120 # ── Tier 1 — Hash functions return canonical form ─────────────────────────────
121
122 class TestHashFunctionsReturnCanonicalForm:
123 """compute_snapshot_id and compute_commit_id must return sha256:<64-hex> strings."""
124
125 def test_compute_snapshot_id_hub_returns_canonical(self) -> None:
126 result = compute_snapshot_id(_MANIFEST, _DIRS)
127 assert _CANONICAL_ID_RE.match(result), (
128 f"compute_snapshot_id (hub) returned {result!r}, "
129 f"expected sha256:<64 lowercase hex chars>"
130 )
131
132 def test_compute_commit_id_hub_returns_canonical(self) -> None:
133 snap_id = compute_snapshot_id(_MANIFEST, _DIRS)
134 result = compute_commit_id(_PARENT_IDS, snap_id, _MESSAGE, _TIMESTAMP)
135 assert _CANONICAL_ID_RE.match(result), (
136 f"compute_commit_id (hub) returned {result!r}, "
137 f"expected sha256:<64 lowercase hex chars>"
138 )
139
140 def test_compute_snapshot_id_cli_returns_canonical(self) -> None:
141 result = cli_compute_snapshot_id(_MANIFEST, _DIRS)
142 assert _CANONICAL_ID_RE.match(result), (
143 f"compute_snapshot_id (cli) returned {result!r}, "
144 f"expected sha256:<64 lowercase hex chars>"
145 )
146
147 def test_compute_commit_id_cli_returns_canonical(self) -> None:
148 snap_id = cli_compute_snapshot_id(_MANIFEST, _DIRS)
149 result = cli_compute_commit_id(_PARENT_IDS, snap_id, _MESSAGE, _TIMESTAMP)
150 assert _CANONICAL_ID_RE.match(result), (
151 f"compute_commit_id (cli) returned {result!r}, "
152 f"expected sha256:<64 lowercase hex chars>"
153 )
154
155 def test_hub_and_cli_snapshot_id_agree(self) -> None:
156 """Both implementations must produce identical output for the same manifest."""
157 hub_result = compute_snapshot_id(_MANIFEST, _DIRS)
158 cli_result = cli_compute_snapshot_id(_MANIFEST, _DIRS)
159 assert hub_result == cli_result, (
160 f"snapshot_id mismatch: hub={hub_result!r}, cli={cli_result!r}"
161 )
162
163 def test_hub_and_cli_commit_id_agree(self) -> None:
164 """Both implementations must produce identical output for the same inputs."""
165 hub_snap = compute_snapshot_id(_MANIFEST, _DIRS)
166 cli_snap = cli_compute_snapshot_id(_MANIFEST, _DIRS)
167 # snapshot_ids must match first (tested separately), use hub value as input.
168 hub_commit = compute_commit_id(_PARENT_IDS, hub_snap, _MESSAGE, _TIMESTAMP)
169 cli_commit = cli_compute_commit_id(_PARENT_IDS, cli_snap, _MESSAGE, _TIMESTAMP)
170 assert hub_commit == cli_commit, (
171 f"commit_id mismatch: hub={hub_commit!r}, cli={cli_commit!r}"
172 )
173
174 def test_snapshot_id_is_deterministic(self) -> None:
175 """Same inputs always produce the same snapshot_id."""
176 a = compute_snapshot_id(_MANIFEST, _DIRS)
177 b = compute_snapshot_id(_MANIFEST, _DIRS)
178 assert a == b
179
180 def test_commit_id_is_deterministic(self) -> None:
181 """Same inputs always produce the same commit_id."""
182 snap = compute_snapshot_id(_MANIFEST, _DIRS)
183 a = compute_commit_id(_PARENT_IDS, snap, _MESSAGE, _TIMESTAMP)
184 b = compute_commit_id(_PARENT_IDS, snap, _MESSAGE, _TIMESTAMP)
185 assert a == b
186
187 def test_snapshot_id_without_dirs_returns_canonical(self) -> None:
188 """Omitting the directories argument still returns canonical form."""
189 result = compute_snapshot_id(_MANIFEST)
190 assert _CANONICAL_ID_RE.match(result), (
191 f"compute_snapshot_id (no dirs) returned {result!r}"
192 )
193
194 def test_snapshot_id_empty_manifest_returns_canonical(self) -> None:
195 """An empty manifest produces a canonical ID (not an error)."""
196 result = compute_snapshot_id({})
197 assert _CANONICAL_ID_RE.match(result), (
198 f"compute_snapshot_id (empty manifest) returned {result!r}"
199 )
200
201
202 # ── Tier 2 — WireCommit validation ────────────────────────────────────────────
203
204 class TestWireCommitValidation:
205 """WireCommit must enforce sha256: prefix on commit_id, snapshot_id, and parent_commit_id."""
206
207 def test_canonical_commit_id_accepted(self) -> None:
208 commit = WireCommit(
209 commit_id=_VALID_COMMIT_ID,
210 snapshot_id=_VALID_SNAP_ID,
211 )
212 assert commit.commit_id == _VALID_COMMIT_ID
213
214 def test_bare_hex_commit_id_rejected(self) -> None:
215 """A 64-char hex commit_id without the sha256: prefix must be rejected."""
216 with pytest.raises((ValueError, ValidationError)):
217 WireCommit(
218 commit_id=_BARE_HEX_64,
219 snapshot_id=_VALID_SNAP_ID,
220 )
221
222 def test_uuid_commit_id_rejected(self) -> None:
223 """A UUID commit_id is not a content-addressed ID and must be rejected."""
224 with pytest.raises((ValueError, ValidationError)):
225 WireCommit(
226 commit_id=_UUID_ID,
227 snapshot_id=_VALID_SNAP_ID,
228 )
229
230 def test_bare_hex_snapshot_id_rejected(self) -> None:
231 """WireCommit.snapshot_id must also carry the sha256: prefix."""
232 with pytest.raises((ValueError, ValidationError)):
233 WireCommit(
234 commit_id=_VALID_COMMIT_ID,
235 snapshot_id=_BARE_HEX_64,
236 )
237
238 def test_canonical_parent_commit_id_accepted(self) -> None:
239 """A canonical parent_commit_id is valid."""
240 commit = WireCommit(
241 commit_id=_VALID_COMMIT_ID,
242 snapshot_id=_VALID_SNAP_ID,
243 parent_commit_id=_VALID_PARENT_ID,
244 )
245 assert commit.parent_commit_id == _VALID_PARENT_ID
246
247 def test_none_parent_commit_id_accepted(self) -> None:
248 """parent_commit_id=None is valid (root commit)."""
249 commit = WireCommit(
250 commit_id=_VALID_COMMIT_ID,
251 snapshot_id=_VALID_SNAP_ID,
252 parent_commit_id=None,
253 )
254 assert commit.parent_commit_id is None
255
256 def test_bare_hex_parent_commit_id_rejected(self) -> None:
257 """parent_commit_id when non-None must carry the sha256: prefix."""
258 with pytest.raises((ValueError, ValidationError)):
259 WireCommit(
260 commit_id=_VALID_COMMIT_ID,
261 snapshot_id=_VALID_SNAP_ID,
262 parent_commit_id=_BARE_HEX_64,
263 )
264
265
266 # ── Tier 3 — WireSnapshot validation ──────────────────────────────────────────
267
268 class TestWireSnapshotValidation:
269 """WireSnapshot must enforce sha256: prefix on snapshot_id."""
270
271 def test_canonical_snapshot_id_accepted(self) -> None:
272 snap = WireSnapshot(snapshot_id=_VALID_SNAP_ID)
273 assert snap.snapshot_id == _VALID_SNAP_ID
274
275 def test_bare_hex_snapshot_id_rejected(self) -> None:
276 """A 64-char hex snapshot_id without the sha256: prefix must be rejected."""
277 with pytest.raises((ValueError, ValidationError)):
278 WireSnapshot(snapshot_id=_BARE_HEX_64)
279
280 def test_uuid_snapshot_id_rejected(self) -> None:
281 """A UUID snapshot_id is not a content-addressed ID and must be rejected."""
282 with pytest.raises((ValueError, ValidationError)):
283 WireSnapshot(snapshot_id=_UUID_ID)
284
285 def test_canonical_snapshot_with_manifest_accepted(self) -> None:
286 snap = WireSnapshot(
287 snapshot_id=_VALID_SNAP_ID,
288 manifest={"README.md": _VALID_OBJECT_ID},
289 )
290 assert snap.snapshot_id == _VALID_SNAP_ID
291 assert "README.md" in snap.manifest
292
293
294 # ── Tier 4 — Wire push endpoint rejects non-canonical IDs ─────────────────────
295
296 @pytest.mark.asyncio
297 async def test_push_with_canonical_ids_returns_200(
298 client: AsyncClient,
299 db_session: AsyncSession,
300 wire_headers: dict[str, str],
301 ) -> None:
302 """A push with fully canonical sha256: IDs throughout must succeed (200)."""
303 repo = await create_repo(db_session, slug="canonical-ids-ok", owner="test-user-wire")
304 payload = _make_push_payload(
305 repo.repo_id,
306 commit_id=_VALID_COMMIT_ID,
307 snapshot_id=_VALID_SNAP_ID,
308 )
309 resp = await client.post(
310 f"/{repo.owner}/{repo.slug}/push",
311 content=_mp(payload),
312 headers=wire_headers,
313 )
314 assert resp.status_code == 200, (
315 f"Expected 200 for canonical IDs but got {resp.status_code}: {resp.text}"
316 )
317
318
319 @pytest.mark.asyncio
320 async def test_push_with_bare_hex_commit_id_returns_422(
321 client: AsyncClient,
322 db_session: AsyncSession,
323 wire_headers: dict[str, str],
324 ) -> None:
325 """A push where commit_id is bare hex (no sha256: prefix) must return 422."""
326 repo = await create_repo(db_session, slug="bare-hex-commit-422", owner="test-user-wire")
327 payload = _make_push_payload(
328 repo.repo_id,
329 commit_id=_BARE_HEX_64,
330 snapshot_id=_VALID_SNAP_ID,
331 )
332 resp = await client.post(
333 f"/{repo.owner}/{repo.slug}/push",
334 content=_mp(payload),
335 headers=wire_headers,
336 )
337 assert resp.status_code == 422, (
338 f"Expected 422 for bare-hex commit_id but got {resp.status_code}: {resp.text}"
339 )
340
341
342 @pytest.mark.asyncio
343 async def test_push_with_bare_hex_snapshot_id_returns_422(
344 client: AsyncClient,
345 db_session: AsyncSession,
346 wire_headers: dict[str, str],
347 ) -> None:
348 """A push where snapshot_id is bare hex (no sha256: prefix) must return 422."""
349 repo = await create_repo(db_session, slug="bare-hex-snap-422", owner="test-user-wire")
350 payload = _make_push_payload(
351 repo.repo_id,
352 commit_id=_VALID_COMMIT_ID,
353 snapshot_id=_BARE_HEX_64,
354 )
355 resp = await client.post(
356 f"/{repo.owner}/{repo.slug}/push",
357 content=_mp(payload),
358 headers=wire_headers,
359 )
360 assert resp.status_code == 422, (
361 f"Expected 422 for bare-hex snapshot_id but got {resp.status_code}: {resp.text}"
362 )
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago