gabriel / musehub public
test_id_canonical.py python
486 lines 19.5 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 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 from datetime import datetime, timezone
20 from pathlib import Path
21
22 import msgpack
23 import pytest
24 from httpx import AsyncClient
25 from pydantic import ValidationError
26 from sqlalchemy.ext.asyncio import AsyncSession
27
28 from musehub.muse_cli.snapshot import compute_snapshot_id, compute_commit_id
29 from musehub.types.json_types import JSONObject, JSONValue
30 from musehub.models.wire import WireCommit, WireSnapshot, WireSnapshotDelta
31
32 # Cross-verify against the Muse CLI implementation.
33 sys.path.insert(0, str(Path.home() / "ecosystem" / "muse"))
34 from muse.core.mpack import WIRE_CONTENT_TYPE, MuseWireFrameWriter
35 from muse.core.snapshot import compute_snapshot_id as cli_compute_snapshot_id
36 from muse.core.snapshot import compute_commit_id as cli_compute_commit_id
37
38 from muse.core.types import long_id, now_utc_iso
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": long_id("a" * 64),
49 "src/main.py": long_id("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
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 # Plain string — not a content-addressed ID, also invalid.
74 _PLAIN_ID = "not-a-content-addressed-id"
75
76
77 # ── Helpers ───────────────────────────────────────────────────────────────────
78
79
80
81 _fw = MuseWireFrameWriter()
82
83 _SFRAME_HEADER = "H"
84 _SFRAME_COMMIT_PACK = "C"
85 _SFRAME_END = "E"
86 _SFRAME_ERROR = "X"
87 _SFRAME_RESULT = "R"
88
89
90 def _wrap(ft: str, data: JSONValue) -> bytes:
91 return _fw.wrap(frame_type=ft, payload=msgpack.packb(data, use_bin_type=True))
92
93
94 def _make_mwp_push(
95 *,
96 commit_id: str = _VALID_COMMIT_ID,
97 snapshot_id: str = _VALID_SNAP_ID,
98 ) -> bytes:
99 """Build a minimal MWP push body with controllable commit_id and snapshot_id."""
100 header = _wrap(_SFRAME_HEADER, {
101 "t": _SFRAME_HEADER, "branch": "main", "force": False,
102 "have": [], "head": commit_id, "n_objects": 0, "n_commits": 1,
103 })
104 commit_pack = _wrap(_SFRAME_COMMIT_PACK, {
105 "t": _SFRAME_COMMIT_PACK,
106 "commits": [{
107 "commit_id": commit_id,
108 "parent_commit_id": None,
109 "parent2_commit_id": None,
110 "snapshot_id": snapshot_id,
111 "branch": "main",
112 "message": "test: canonical ID push",
113 "author": "Test User <[email protected]>",
114 "committed_at": now_utc_iso(),
115 "signature": "",
116 "signer_key_id": "",
117 "agent_id": "",
118 "model_id": "",
119 "metadata": {},
120 }],
121 "snapshots": [{"snapshot_id": snapshot_id, "manifest": {}, "committed_at": now_utc_iso()}],
122 })
123 end = _wrap(_SFRAME_END, {"t": _SFRAME_END, "n_objects": 0, "n_commits": 1})
124 return header + commit_pack + end
125
126
127 def _parse_mwp_result(raw: bytes) -> JSONObject:
128 """Return the last msgpack frame from an MWP response stream."""
129 unpacker = msgpack.Unpacker(raw=False)
130 unpacker.feed(raw)
131 last: JSONObject = {}
132 for frame in unpacker:
133 last = frame
134 return last
135
136
137 # ── Tier 1 — Hash functions return canonical form ─────────────────────────────
138
139 class TestHashFunctionsReturnCanonicalForm:
140 """compute_snapshot_id and compute_commit_id must return sha256:<64-hex> strings."""
141
142 def test_compute_snapshot_id_hub_returns_canonical(self) -> None:
143 result = compute_snapshot_id(_MANIFEST, _DIRS)
144 assert _CANONICAL_ID_RE.match(result), (
145 f"compute_snapshot_id (hub) returned {result!r}, "
146 f"expected sha256:<64 lowercase hex chars>"
147 )
148
149 def test_compute_commit_id_hub_returns_canonical(self) -> None:
150 snap_id = compute_snapshot_id(_MANIFEST, _DIRS)
151 result = compute_commit_id(_PARENT_IDS, snap_id, _MESSAGE, _TIMESTAMP)
152 assert _CANONICAL_ID_RE.match(result), (
153 f"compute_commit_id (hub) returned {result!r}, "
154 f"expected sha256:<64 lowercase hex chars>"
155 )
156
157 def test_compute_snapshot_id_cli_returns_canonical(self) -> None:
158 result = cli_compute_snapshot_id(_MANIFEST, _DIRS)
159 assert _CANONICAL_ID_RE.match(result), (
160 f"compute_snapshot_id (cli) returned {result!r}, "
161 f"expected sha256:<64 lowercase hex chars>"
162 )
163
164 def test_compute_commit_id_cli_returns_canonical(self) -> None:
165 snap_id = cli_compute_snapshot_id(_MANIFEST, _DIRS)
166 result = cli_compute_commit_id(_PARENT_IDS, snap_id, _MESSAGE, _TIMESTAMP)
167 assert _CANONICAL_ID_RE.match(result), (
168 f"compute_commit_id (cli) returned {result!r}, "
169 f"expected sha256:<64 lowercase hex chars>"
170 )
171
172 def test_hub_and_cli_snapshot_id_agree(self) -> None:
173 """Both implementations must produce identical output for the same manifest."""
174 hub_result = compute_snapshot_id(_MANIFEST, _DIRS)
175 cli_result = cli_compute_snapshot_id(_MANIFEST, _DIRS)
176 assert hub_result == cli_result, (
177 f"snapshot_id mismatch: hub={hub_result!r}, cli={cli_result!r}"
178 )
179
180 def test_hub_and_cli_commit_id_agree(self) -> None:
181 """Both implementations must produce identical output for the same inputs."""
182 hub_snap = compute_snapshot_id(_MANIFEST, _DIRS)
183 cli_snap = cli_compute_snapshot_id(_MANIFEST, _DIRS)
184 # snapshot_ids must match first (tested separately), use hub value as input.
185 hub_commit = compute_commit_id(_PARENT_IDS, hub_snap, _MESSAGE, _TIMESTAMP)
186 cli_commit = cli_compute_commit_id(_PARENT_IDS, cli_snap, _MESSAGE, _TIMESTAMP)
187 assert hub_commit == cli_commit, (
188 f"commit_id mismatch: hub={hub_commit!r}, cli={cli_commit!r}"
189 )
190
191 def test_snapshot_id_is_deterministic(self) -> None:
192 """Same inputs always produce the same snapshot_id."""
193 a = compute_snapshot_id(_MANIFEST, _DIRS)
194 b = compute_snapshot_id(_MANIFEST, _DIRS)
195 assert a == b
196
197 def test_commit_id_is_deterministic(self) -> None:
198 """Same inputs always produce the same commit_id."""
199 snap = compute_snapshot_id(_MANIFEST, _DIRS)
200 a = compute_commit_id(_PARENT_IDS, snap, _MESSAGE, _TIMESTAMP)
201 b = compute_commit_id(_PARENT_IDS, snap, _MESSAGE, _TIMESTAMP)
202 assert a == b
203
204 def test_snapshot_id_without_dirs_returns_canonical(self) -> None:
205 """Omitting the directories argument still returns canonical form."""
206 result = compute_snapshot_id(_MANIFEST)
207 assert _CANONICAL_ID_RE.match(result), (
208 f"compute_snapshot_id (no dirs) returned {result!r}"
209 )
210
211 def test_snapshot_id_empty_manifest_returns_canonical(self) -> None:
212 """An empty manifest produces a canonical ID (not an error)."""
213 result = compute_snapshot_id({})
214 assert _CANONICAL_ID_RE.match(result), (
215 f"compute_snapshot_id (empty manifest) returned {result!r}"
216 )
217
218
219 # ── Tier 2 — WireCommit validation ────────────────────────────────────────────
220
221 class TestWireCommitValidation:
222 """WireCommit must enforce sha256: prefix on commit_id, snapshot_id, and parent_commit_id."""
223
224 def test_canonical_commit_id_accepted(self) -> None:
225 commit = WireCommit(
226 commit_id=_VALID_COMMIT_ID,
227 snapshot_id=_VALID_SNAP_ID,
228 )
229 assert commit.commit_id == _VALID_COMMIT_ID
230
231 def test_bare_hex_commit_id_rejected(self) -> None:
232 """A 64-char hex commit_id without the sha256: prefix must be rejected."""
233 with pytest.raises((ValueError, ValidationError)):
234 WireCommit(
235 commit_id=_BARE_HEX_64,
236 snapshot_id=_VALID_SNAP_ID,
237 )
238
239 def test_plain_id_commit_id_rejected(self) -> None:
240 """A plain string commit_id is not a content-addressed ID and must be rejected."""
241 with pytest.raises((ValueError, ValidationError)):
242 WireCommit(
243 commit_id=_PLAIN_ID,
244 snapshot_id=_VALID_SNAP_ID,
245 )
246
247 def test_bare_hex_snapshot_id_rejected(self) -> None:
248 """WireCommit.snapshot_id must also carry the sha256: prefix."""
249 with pytest.raises((ValueError, ValidationError)):
250 WireCommit(
251 commit_id=_VALID_COMMIT_ID,
252 snapshot_id=_BARE_HEX_64,
253 )
254
255 def test_canonical_parent_commit_id_accepted(self) -> None:
256 """A canonical parent_commit_id is valid."""
257 commit = WireCommit(
258 commit_id=_VALID_COMMIT_ID,
259 snapshot_id=_VALID_SNAP_ID,
260 parent_commit_id=_VALID_PARENT_ID,
261 )
262 assert commit.parent_commit_id == _VALID_PARENT_ID
263
264 def test_none_parent_commit_id_accepted(self) -> None:
265 """parent_commit_id=None is valid (root commit)."""
266 commit = WireCommit(
267 commit_id=_VALID_COMMIT_ID,
268 snapshot_id=_VALID_SNAP_ID,
269 parent_commit_id=None,
270 )
271 assert commit.parent_commit_id is None
272
273 def test_bare_hex_parent_commit_id_rejected(self) -> None:
274 """parent_commit_id when non-None must carry the sha256: prefix."""
275 with pytest.raises((ValueError, ValidationError)):
276 WireCommit(
277 commit_id=_VALID_COMMIT_ID,
278 snapshot_id=_VALID_SNAP_ID,
279 parent_commit_id=_BARE_HEX_64,
280 )
281
282
283 # ── Tier 3 — WireSnapshot validation ──────────────────────────────────────────
284
285 class TestWireSnapshotValidation:
286 """WireSnapshot must enforce sha256: prefix on snapshot_id."""
287
288 def test_canonical_snapshot_id_accepted(self) -> None:
289 snap = WireSnapshot(snapshot_id=_VALID_SNAP_ID)
290 assert snap.snapshot_id == _VALID_SNAP_ID
291
292 def test_bare_hex_snapshot_id_rejected(self) -> None:
293 """A 64-char hex snapshot_id without the sha256: prefix must be rejected."""
294 with pytest.raises((ValueError, ValidationError)):
295 WireSnapshot(snapshot_id=_BARE_HEX_64)
296
297 def test_plain_id_snapshot_id_rejected(self) -> None:
298 """A plain string snapshot_id is not a content-addressed ID and must be rejected."""
299 with pytest.raises((ValueError, ValidationError)):
300 WireSnapshot(snapshot_id=_PLAIN_ID)
301
302 def test_canonical_snapshot_with_manifest_accepted(self) -> None:
303 snap = WireSnapshot(
304 snapshot_id=_VALID_SNAP_ID,
305 manifest={"README.md": _VALID_OBJECT_ID},
306 )
307 assert snap.snapshot_id == _VALID_SNAP_ID
308 assert "README.md" in snap.manifest
309
310 def test_manifest_bare_hex_object_id_rejected(self) -> None:
311 """Manifest values that are bare hex (no sha256: prefix) must be rejected."""
312 with pytest.raises((ValueError, ValidationError)):
313 WireSnapshot(
314 snapshot_id=_VALID_SNAP_ID,
315 manifest={"src/main.py": _BARE_HEX_64},
316 )
317
318 def test_manifest_plain_id_object_id_rejected(self) -> None:
319 """Manifest values that are plain strings (not sha256:) must be rejected."""
320 with pytest.raises((ValueError, ValidationError)):
321 WireSnapshot(
322 snapshot_id=_VALID_SNAP_ID,
323 manifest={"src/main.py": _PLAIN_ID},
324 )
325
326 def test_manifest_multiple_entries_all_canonical(self) -> None:
327 """All manifest values must be validated — not just the first."""
328 with pytest.raises((ValueError, ValidationError)):
329 WireSnapshot(
330 snapshot_id=_VALID_SNAP_ID,
331 manifest={
332 "README.md": _VALID_OBJECT_ID, # good
333 "src/main.py": _BARE_HEX_64, # bad — second entry
334 },
335 )
336
337
338 class TestWireSnapshotDeltaValidation:
339 """WireSnapshotDelta.added values must carry the sha256: prefix."""
340
341 def test_canonical_delta_accepted(self) -> None:
342 delta = WireSnapshotDelta(
343 snapshot_id=_VALID_SNAP_ID,
344 base_id=_VALID_SNAP_ID,
345 added={"src/new.py": _VALID_OBJECT_ID},
346 )
347 assert delta.added["src/new.py"] == _VALID_OBJECT_ID
348
349 def test_added_bare_hex_object_id_rejected(self) -> None:
350 """added values that are bare hex (no sha256: prefix) must be rejected."""
351 with pytest.raises((ValueError, ValidationError)):
352 WireSnapshotDelta(
353 snapshot_id=_VALID_SNAP_ID,
354 base_id=_VALID_SNAP_ID,
355 added={"src/new.py": _BARE_HEX_64},
356 )
357
358 def test_added_plain_id_object_id_rejected(self) -> None:
359 """added values that are plain strings (not sha256:) must be rejected."""
360 with pytest.raises((ValueError, ValidationError)):
361 WireSnapshotDelta(
362 snapshot_id=_VALID_SNAP_ID,
363 base_id=_VALID_SNAP_ID,
364 added={"src/new.py": _PLAIN_ID},
365 )
366
367 def test_added_multiple_entries_all_validated(self) -> None:
368 """All added values must be validated — not just the first."""
369 with pytest.raises((ValueError, ValidationError)):
370 WireSnapshotDelta(
371 snapshot_id=_VALID_SNAP_ID,
372 base_id=_VALID_SNAP_ID,
373 added={
374 "README.md": _VALID_OBJECT_ID, # good
375 "src/bad.py": _BARE_HEX_64, # bad
376 },
377 )
378
379 def test_empty_added_accepted(self) -> None:
380 """An empty added dict (no new files) is valid."""
381 delta = WireSnapshotDelta(
382 snapshot_id=_VALID_SNAP_ID,
383 base_id=_VALID_SNAP_ID,
384 )
385 assert delta.added == {}
386
387
388 class TestWireCommitPromptHashValidation:
389 """WireCommit.prompt_hash must be empty or sha256:<64-hex>."""
390
391 def test_empty_prompt_hash_accepted(self) -> None:
392 commit = WireCommit(commit_id=_VALID_COMMIT_ID, prompt_hash="")
393 assert commit.prompt_hash == ""
394
395 def test_canonical_prompt_hash_accepted(self) -> None:
396 commit = WireCommit(
397 commit_id=_VALID_COMMIT_ID,
398 prompt_hash=_VALID_OBJECT_ID,
399 )
400 assert commit.prompt_hash == _VALID_OBJECT_ID
401
402 def test_bare_hex_prompt_hash_rejected(self) -> None:
403 """A bare 64-char hex prompt_hash without sha256: prefix must be rejected."""
404 with pytest.raises((ValueError, ValidationError)):
405 WireCommit(
406 commit_id=_VALID_COMMIT_ID,
407 prompt_hash=_BARE_HEX_64,
408 )
409
410 def test_arbitrary_string_prompt_hash_rejected(self) -> None:
411 """An arbitrary string prompt_hash must be rejected."""
412 with pytest.raises((ValueError, ValidationError)):
413 WireCommit(
414 commit_id=_VALID_COMMIT_ID,
415 prompt_hash="abc123",
416 )
417
418
419 # ── Tier 4 — Wire push/stream endpoint rejects non-canonical IDs ──────────────
420
421 @pytest.mark.asyncio
422 async def test_push_with_canonical_ids_returns_ok(
423 client: AsyncClient,
424 db_session: AsyncSession,
425 auth_headers: dict[str, str],
426 monkeypatch: pytest.MonkeyPatch,
427 ) -> None:
428 """A push with fully canonical sha256: IDs throughout must succeed (ok=True result frame)."""
429 from tests.test_wire_push_stream import _stub_r2_backend
430 _stub_r2_backend(monkeypatch)
431 repo = await create_repo(db_session, slug="canonical-ids-ok", owner="testuser")
432 body = _make_mwp_push(commit_id=_VALID_COMMIT_ID, snapshot_id=_VALID_SNAP_ID)
433 resp = await client.post(
434 f"/{repo.owner}/{repo.slug}/push/stream",
435 content=body,
436 headers={**auth_headers, "Content-Type": WIRE_CONTENT_TYPE},
437 )
438 assert resp.status_code == 200, f"Expected 200 but got {resp.status_code}: {resp.text}"
439 result = _parse_mwp_result(resp.content)
440 assert result.get("ok") is True, f"Expected ok=True result frame, got: {result}"
441
442
443 @pytest.mark.asyncio
444 async def test_push_with_bare_hex_commit_id_returns_error(
445 client: AsyncClient,
446 db_session: AsyncSession,
447 auth_headers: dict[str, str],
448 monkeypatch: pytest.MonkeyPatch,
449 ) -> None:
450 """A push where commit_id is bare hex (no sha256: prefix) must be rejected."""
451 from tests.test_wire_push_stream import _stub_r2_backend
452 _stub_r2_backend(monkeypatch)
453 repo = await create_repo(db_session, slug="bare-hex-commit-err", owner="testuser")
454 body = _make_mwp_push(commit_id=_BARE_HEX_64, snapshot_id=_VALID_SNAP_ID)
455 resp = await client.post(
456 f"/{repo.owner}/{repo.slug}/push/stream",
457 content=body,
458 headers={**auth_headers, "Content-Type": WIRE_CONTENT_TYPE},
459 )
460 assert resp.status_code in (200, 422), f"Expected rejection but got {resp.status_code}"
461 if resp.status_code == 200:
462 result = _parse_mwp_result(resp.content)
463 assert result.get("ok") is not True, f"Expected rejection, got ok=True: {result}"
464
465
466 @pytest.mark.asyncio
467 async def test_push_with_bare_hex_snapshot_id_returns_error(
468 client: AsyncClient,
469 db_session: AsyncSession,
470 auth_headers: dict[str, str],
471 monkeypatch: pytest.MonkeyPatch,
472 ) -> None:
473 """A push where snapshot_id is bare hex (no sha256: prefix) must be rejected."""
474 from tests.test_wire_push_stream import _stub_r2_backend
475 _stub_r2_backend(monkeypatch)
476 repo = await create_repo(db_session, slug="bare-hex-snap-err", owner="testuser")
477 body = _make_mwp_push(commit_id=_VALID_COMMIT_ID, snapshot_id=_BARE_HEX_64)
478 resp = await client.post(
479 f"/{repo.owner}/{repo.slug}/push/stream",
480 content=body,
481 headers={**auth_headers, "Content-Type": WIRE_CONTENT_TYPE},
482 )
483 assert resp.status_code in (200, 422), f"Expected rejection but got {resp.status_code}"
484 if resp.status_code == 200:
485 result = _parse_mwp_result(resp.content)
486 assert result.get("ok") is not True, f"Expected rejection, got ok=True: {result}"
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago