gabriel / musehub public
test_id_canonical.py python
380 lines 15.3 KB
Raw
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor ⚠ breaking 143 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.types.json_types import JSONObject, JSONValue
31 from musehub.models.wire import WireCommit, WireSnapshot
32
33 # Cross-verify against the Muse CLI implementation.
34 sys.path.insert(0, str(Path.home() / "ecosystem" / "muse"))
35 from muse.core.mpack import WIRE_CONTENT_TYPE, MuseWireFrameWriter
36 from muse.core.snapshot import compute_snapshot_id as cli_compute_snapshot_id
37 from muse.core.snapshot import compute_commit_id as cli_compute_commit_id
38
39 from tests.factories import create_repo
40
41 # ── Module-level canonical ID regex ───────────────────────────────────────────
42
43 _CANONICAL_ID_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
44
45 # ── Shared test inputs (deterministic) ────────────────────────────────────────
46
47 _MANIFEST: dict[str, str] = {
48 "README.md": f"sha256:{'a' * 64}",
49 "src/main.py": f"sha256:{'b' * 64}",
50 }
51 _DIRS: list[str] = ["src"]
52 _MESSAGE = "feat: add canonical ID tests"
53 _TIMESTAMP = "2026-01-01T00:00:00+00:00"
54 _PARENT_IDS: list[str] = []
55
56 # Pre-computed IDs used across tiers.
57 _SNAP_ID_HUB = compute_snapshot_id(_MANIFEST, _DIRS)
58 _COMMIT_ID_HUB = compute_commit_id(_PARENT_IDS, _SNAP_ID_HUB, _MESSAGE, _TIMESTAMP)
59
60 # A valid canonical ID — a real sha256 hex wrapped in the prefix.
61 from muse.core.types import fake_id, blob_id
62 _VALID_SNAP_ID = fake_id("snapshot-fixture")
63 _VALID_COMMIT_ID = fake_id("commit-fixture")
64 _VALID_PARENT_ID = fake_id("parent-fixture")
65 _VALID_OBJECT_ID = fake_id("object-fixture")
66
67 # Bare hex (no prefix) — always invalid in the canonical contract.
68 # Strip the sha256: prefix to produce deliberately unprefixed hex.
69 from muse.core.types import long_id as _long_id
70 _BARE_HEX_64 = _long_id(fake_id("bare"), strip=True) # 64-char hex, no prefix
71 assert len(_BARE_HEX_64) == 64
72
73 # UUID string — also invalid.
74 _UUID_ID = str(uuid.uuid4())
75
76
77 # ── Helpers ───────────────────────────────────────────────────────────────────
78
79 def _utc_now() -> str:
80 return datetime.now(tz=timezone.utc).isoformat()
81
82
83 _fw = MuseWireFrameWriter()
84
85 _SFRAME_HEADER = "H"
86 _SFRAME_COMMIT_PACK = "C"
87 _SFRAME_END = "E"
88 _SFRAME_ERROR = "X"
89 _SFRAME_RESULT = "R"
90
91
92 def _wrap(ft: str, data: JSONValue) -> bytes:
93 return _fw.wrap(frame_type=ft, payload=msgpack.packb(data, use_bin_type=True))
94
95
96 def _make_mwp_push(
97 *,
98 commit_id: str = _VALID_COMMIT_ID,
99 snapshot_id: str = _VALID_SNAP_ID,
100 ) -> bytes:
101 """Build a minimal MWP push body with controllable commit_id and snapshot_id."""
102 header = _wrap(_SFRAME_HEADER, {
103 "t": _SFRAME_HEADER, "branch": "main", "force": False,
104 "have": [], "head": commit_id, "n_objects": 0, "n_commits": 1,
105 })
106 commit_pack = _wrap(_SFRAME_COMMIT_PACK, {
107 "t": _SFRAME_COMMIT_PACK,
108 "commits": [{
109 "commit_id": commit_id,
110 "parent_commit_id": None,
111 "parent2_commit_id": None,
112 "snapshot_id": snapshot_id,
113 "branch": "main",
114 "message": "test: canonical ID push",
115 "author": "Test User <[email protected]>",
116 "committed_at": _utc_now(),
117 "signature": "",
118 "signer_key_id": "",
119 "agent_id": "",
120 "model_id": "",
121 "metadata": {},
122 }],
123 "snapshots": [{"snapshot_id": snapshot_id, "manifest": {}, "committed_at": _utc_now()}],
124 })
125 end = _wrap(_SFRAME_END, {"t": _SFRAME_END, "n_objects": 0, "n_commits": 1})
126 return header + commit_pack + end
127
128
129 def _parse_mwp_result(raw: bytes) -> JSONObject:
130 """Return the last msgpack frame from an MWP response stream."""
131 unpacker = msgpack.Unpacker(raw=False)
132 unpacker.feed(raw)
133 last: JSONObject = {}
134 for frame in unpacker:
135 last = frame
136 return last
137
138
139 # ── Tier 1 — Hash functions return canonical form ─────────────────────────────
140
141 class TestHashFunctionsReturnCanonicalForm:
142 """compute_snapshot_id and compute_commit_id must return sha256:<64-hex> strings."""
143
144 def test_compute_snapshot_id_hub_returns_canonical(self) -> None:
145 result = compute_snapshot_id(_MANIFEST, _DIRS)
146 assert _CANONICAL_ID_RE.match(result), (
147 f"compute_snapshot_id (hub) returned {result!r}, "
148 f"expected sha256:<64 lowercase hex chars>"
149 )
150
151 def test_compute_commit_id_hub_returns_canonical(self) -> None:
152 snap_id = compute_snapshot_id(_MANIFEST, _DIRS)
153 result = compute_commit_id(_PARENT_IDS, snap_id, _MESSAGE, _TIMESTAMP)
154 assert _CANONICAL_ID_RE.match(result), (
155 f"compute_commit_id (hub) returned {result!r}, "
156 f"expected sha256:<64 lowercase hex chars>"
157 )
158
159 def test_compute_snapshot_id_cli_returns_canonical(self) -> None:
160 result = cli_compute_snapshot_id(_MANIFEST, _DIRS)
161 assert _CANONICAL_ID_RE.match(result), (
162 f"compute_snapshot_id (cli) returned {result!r}, "
163 f"expected sha256:<64 lowercase hex chars>"
164 )
165
166 def test_compute_commit_id_cli_returns_canonical(self) -> None:
167 snap_id = cli_compute_snapshot_id(_MANIFEST, _DIRS)
168 result = cli_compute_commit_id(_PARENT_IDS, snap_id, _MESSAGE, _TIMESTAMP)
169 assert _CANONICAL_ID_RE.match(result), (
170 f"compute_commit_id (cli) returned {result!r}, "
171 f"expected sha256:<64 lowercase hex chars>"
172 )
173
174 def test_hub_and_cli_snapshot_id_agree(self) -> None:
175 """Both implementations must produce identical output for the same manifest."""
176 hub_result = compute_snapshot_id(_MANIFEST, _DIRS)
177 cli_result = cli_compute_snapshot_id(_MANIFEST, _DIRS)
178 assert hub_result == cli_result, (
179 f"snapshot_id mismatch: hub={hub_result!r}, cli={cli_result!r}"
180 )
181
182 def test_hub_and_cli_commit_id_agree(self) -> None:
183 """Both implementations must produce identical output for the same inputs."""
184 hub_snap = compute_snapshot_id(_MANIFEST, _DIRS)
185 cli_snap = cli_compute_snapshot_id(_MANIFEST, _DIRS)
186 # snapshot_ids must match first (tested separately), use hub value as input.
187 hub_commit = compute_commit_id(_PARENT_IDS, hub_snap, _MESSAGE, _TIMESTAMP)
188 cli_commit = cli_compute_commit_id(_PARENT_IDS, cli_snap, _MESSAGE, _TIMESTAMP)
189 assert hub_commit == cli_commit, (
190 f"commit_id mismatch: hub={hub_commit!r}, cli={cli_commit!r}"
191 )
192
193 def test_snapshot_id_is_deterministic(self) -> None:
194 """Same inputs always produce the same snapshot_id."""
195 a = compute_snapshot_id(_MANIFEST, _DIRS)
196 b = compute_snapshot_id(_MANIFEST, _DIRS)
197 assert a == b
198
199 def test_commit_id_is_deterministic(self) -> None:
200 """Same inputs always produce the same commit_id."""
201 snap = compute_snapshot_id(_MANIFEST, _DIRS)
202 a = compute_commit_id(_PARENT_IDS, snap, _MESSAGE, _TIMESTAMP)
203 b = compute_commit_id(_PARENT_IDS, snap, _MESSAGE, _TIMESTAMP)
204 assert a == b
205
206 def test_snapshot_id_without_dirs_returns_canonical(self) -> None:
207 """Omitting the directories argument still returns canonical form."""
208 result = compute_snapshot_id(_MANIFEST)
209 assert _CANONICAL_ID_RE.match(result), (
210 f"compute_snapshot_id (no dirs) returned {result!r}"
211 )
212
213 def test_snapshot_id_empty_manifest_returns_canonical(self) -> None:
214 """An empty manifest produces a canonical ID (not an error)."""
215 result = compute_snapshot_id({})
216 assert _CANONICAL_ID_RE.match(result), (
217 f"compute_snapshot_id (empty manifest) returned {result!r}"
218 )
219
220
221 # ── Tier 2 — WireCommit validation ────────────────────────────────────────────
222
223 class TestWireCommitValidation:
224 """WireCommit must enforce sha256: prefix on commit_id, snapshot_id, and parent_commit_id."""
225
226 def test_canonical_commit_id_accepted(self) -> None:
227 commit = WireCommit(
228 commit_id=_VALID_COMMIT_ID,
229 snapshot_id=_VALID_SNAP_ID,
230 )
231 assert commit.commit_id == _VALID_COMMIT_ID
232
233 def test_bare_hex_commit_id_rejected(self) -> None:
234 """A 64-char hex commit_id without the sha256: prefix must be rejected."""
235 with pytest.raises((ValueError, ValidationError)):
236 WireCommit(
237 commit_id=_BARE_HEX_64,
238 snapshot_id=_VALID_SNAP_ID,
239 )
240
241 def test_uuid_commit_id_rejected(self) -> None:
242 """A UUID commit_id is not a content-addressed ID and must be rejected."""
243 with pytest.raises((ValueError, ValidationError)):
244 WireCommit(
245 commit_id=_UUID_ID,
246 snapshot_id=_VALID_SNAP_ID,
247 )
248
249 def test_bare_hex_snapshot_id_rejected(self) -> None:
250 """WireCommit.snapshot_id must also carry the sha256: prefix."""
251 with pytest.raises((ValueError, ValidationError)):
252 WireCommit(
253 commit_id=_VALID_COMMIT_ID,
254 snapshot_id=_BARE_HEX_64,
255 )
256
257 def test_canonical_parent_commit_id_accepted(self) -> None:
258 """A canonical parent_commit_id is valid."""
259 commit = WireCommit(
260 commit_id=_VALID_COMMIT_ID,
261 snapshot_id=_VALID_SNAP_ID,
262 parent_commit_id=_VALID_PARENT_ID,
263 )
264 assert commit.parent_commit_id == _VALID_PARENT_ID
265
266 def test_none_parent_commit_id_accepted(self) -> None:
267 """parent_commit_id=None is valid (root commit)."""
268 commit = WireCommit(
269 commit_id=_VALID_COMMIT_ID,
270 snapshot_id=_VALID_SNAP_ID,
271 parent_commit_id=None,
272 )
273 assert commit.parent_commit_id is None
274
275 def test_bare_hex_parent_commit_id_rejected(self) -> None:
276 """parent_commit_id when non-None must carry the sha256: prefix."""
277 with pytest.raises((ValueError, ValidationError)):
278 WireCommit(
279 commit_id=_VALID_COMMIT_ID,
280 snapshot_id=_VALID_SNAP_ID,
281 parent_commit_id=_BARE_HEX_64,
282 )
283
284
285 # ── Tier 3 — WireSnapshot validation ──────────────────────────────────────────
286
287 class TestWireSnapshotValidation:
288 """WireSnapshot must enforce sha256: prefix on snapshot_id."""
289
290 def test_canonical_snapshot_id_accepted(self) -> None:
291 snap = WireSnapshot(snapshot_id=_VALID_SNAP_ID)
292 assert snap.snapshot_id == _VALID_SNAP_ID
293
294 def test_bare_hex_snapshot_id_rejected(self) -> None:
295 """A 64-char hex snapshot_id without the sha256: prefix must be rejected."""
296 with pytest.raises((ValueError, ValidationError)):
297 WireSnapshot(snapshot_id=_BARE_HEX_64)
298
299 def test_uuid_snapshot_id_rejected(self) -> None:
300 """A UUID snapshot_id is not a content-addressed ID and must be rejected."""
301 with pytest.raises((ValueError, ValidationError)):
302 WireSnapshot(snapshot_id=_UUID_ID)
303
304 def test_canonical_snapshot_with_manifest_accepted(self) -> None:
305 snap = WireSnapshot(
306 snapshot_id=_VALID_SNAP_ID,
307 manifest={"README.md": _VALID_OBJECT_ID},
308 )
309 assert snap.snapshot_id == _VALID_SNAP_ID
310 assert "README.md" in snap.manifest
311
312
313 # ── Tier 4 — Wire push/stream endpoint rejects non-canonical IDs ──────────────
314
315 @pytest.mark.asyncio
316 async def test_push_with_canonical_ids_returns_ok(
317 client: AsyncClient,
318 db_session: AsyncSession,
319 auth_headers: dict[str, str],
320 monkeypatch: pytest.MonkeyPatch,
321 ) -> None:
322 """A push with fully canonical sha256: IDs throughout must succeed (ok=True result frame)."""
323 from tests.test_wire_push_stream import _stub_r2_backend
324 _stub_r2_backend(monkeypatch)
325 repo = await create_repo(db_session, slug="canonical-ids-ok", owner="testuser")
326 body = _make_mwp_push(commit_id=_VALID_COMMIT_ID, snapshot_id=_VALID_SNAP_ID)
327 resp = await client.post(
328 f"/{repo.owner}/{repo.slug}/push/stream",
329 content=body,
330 headers={**auth_headers, "Content-Type": WIRE_CONTENT_TYPE},
331 )
332 assert resp.status_code == 200, f"Expected 200 but got {resp.status_code}: {resp.text}"
333 result = _parse_mwp_result(resp.content)
334 assert result.get("ok") is True, f"Expected ok=True result frame, got: {result}"
335
336
337 @pytest.mark.asyncio
338 async def test_push_with_bare_hex_commit_id_returns_error(
339 client: AsyncClient,
340 db_session: AsyncSession,
341 auth_headers: dict[str, str],
342 monkeypatch: pytest.MonkeyPatch,
343 ) -> None:
344 """A push where commit_id is bare hex (no sha256: prefix) must be rejected."""
345 from tests.test_wire_push_stream import _stub_r2_backend
346 _stub_r2_backend(monkeypatch)
347 repo = await create_repo(db_session, slug="bare-hex-commit-err", owner="testuser")
348 body = _make_mwp_push(commit_id=_BARE_HEX_64, snapshot_id=_VALID_SNAP_ID)
349 resp = await client.post(
350 f"/{repo.owner}/{repo.slug}/push/stream",
351 content=body,
352 headers={**auth_headers, "Content-Type": WIRE_CONTENT_TYPE},
353 )
354 assert resp.status_code in (200, 422), f"Expected rejection but got {resp.status_code}"
355 if resp.status_code == 200:
356 result = _parse_mwp_result(resp.content)
357 assert result.get("ok") is not True, f"Expected rejection, got ok=True: {result}"
358
359
360 @pytest.mark.asyncio
361 async def test_push_with_bare_hex_snapshot_id_returns_error(
362 client: AsyncClient,
363 db_session: AsyncSession,
364 auth_headers: dict[str, str],
365 monkeypatch: pytest.MonkeyPatch,
366 ) -> None:
367 """A push where snapshot_id is bare hex (no sha256: prefix) must be rejected."""
368 from tests.test_wire_push_stream import _stub_r2_backend
369 _stub_r2_backend(monkeypatch)
370 repo = await create_repo(db_session, slug="bare-hex-snap-err", owner="testuser")
371 body = _make_mwp_push(commit_id=_VALID_COMMIT_ID, snapshot_id=_BARE_HEX_64)
372 resp = await client.post(
373 f"/{repo.owner}/{repo.slug}/push/stream",
374 content=body,
375 headers={**auth_headers, "Content-Type": WIRE_CONTENT_TYPE},
376 )
377 assert resp.status_code in (200, 422), f"Expected rejection but got {resp.status_code}"
378 if resp.status_code == 200:
379 result = _parse_mwp_result(resp.content)
380 assert result.get("ok") is not True, f"Expected rejection, got ok=True: {result}"
File History 1 commit
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor 143 days ago