gabriel / muse public
test_push_object_integrity.py python
140 lines 4.7 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 121 days ago
1 """Tests for push object integrity guard.
2
3 When a snapshot manifest references an object ID that exists in the manifest
4 ("in DB") but the actual object file is missing from the local object store
5 ("missing from storage"), the push must abort with a ValueError rather than
6 proceeding and emitting ok=True.
7
8 Guard location: muse/cli/commands/push.py::_push_stream — the loop over
9 all_object_ids calls read_object; if it returns None and no promisor remote
10 is configured, it raises ValueError.
11 """
12 from __future__ import annotations
13
14 import datetime
15 import json
16 import pathlib
17 from unittest.mock import AsyncMock, MagicMock
18
19 import pytest
20
21 from muse._version import __version__
22 from muse.core.object_store import write_object
23 from muse.core.pack import PushResult, RemoteInfo
24 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
25 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
26 from muse.core.types import blob_id
27 from muse.core.paths import heads_dir, muse_dir
28
29 _REPO_ID = "test-repo"
30 _REMOTE_URL = "https://hub.example.com/repos/test-repo"
31
32
33 # ---------------------------------------------------------------------------
34 # Helpers
35 # ---------------------------------------------------------------------------
36
37
38 def _bare_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
39 dot_muse = muse_dir(tmp_path)
40 for d in ("commits", "snapshots", "objects", "refs/heads", "remotes"):
41 (dot_muse / d).mkdir(parents=True, exist_ok=True)
42 (dot_muse / "HEAD").write_text("ref: refs/heads/main\n")
43 (dot_muse / "repo.json").write_text(
44 json.dumps({"repo_id": _REPO_ID, "schema_version": __version__, "domain": "code"})
45 )
46 # No config.toml / no remotes — ensures no promisor remote is present so
47 # missing objects trigger the ValueError guard rather than being skipped.
48 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
49 monkeypatch.chdir(tmp_path)
50 return tmp_path
51
52
53 def _make_commit(
54 root: pathlib.Path,
55 label: str,
56 parent_id: str | None = None,
57 *,
58 write_objects: bool = True,
59 ) -> CommitRecord:
60 raw = f"content-{label}".encode()
61 oid = blob_id(raw)
62 if write_objects:
63 write_object(root, oid, raw)
64 manifest = {"file.txt": oid}
65 snap_id = compute_snapshot_id(manifest)
66 snap = SnapshotRecord(snapshot_id=snap_id, manifest=manifest)
67 write_snapshot(root, snap)
68 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
69 parent_ids = [parent_id] if parent_id else []
70 cid = compute_commit_id(
71 parent_ids=parent_ids,
72 snapshot_id=snap_id,
73 message=f"commit {label}",
74 committed_at_iso=committed_at.isoformat(),
75 )
76 commit = CommitRecord(
77 repo_id=_REPO_ID,
78 commit_id=cid,
79 branch="main",
80 snapshot_id=snap_id,
81 message=f"commit {label}",
82 committed_at=committed_at,
83 parent_commit_id=parent_id,
84 )
85 write_commit(root, commit)
86 return commit
87
88
89 def _mock_transport(result_head: str) -> MagicMock:
90 transport = MagicMock()
91 transport.fetch_remote_info.return_value = RemoteInfo(
92 domain="code",
93 default_branch="main",
94 branch_heads={},
95 )
96 transport.push_stream_coro = AsyncMock(return_value=PushResult(
97 ok=True,
98 message="ok",
99 branch_heads={"main": result_head},
100 ))
101 return transport
102
103
104 # ---------------------------------------------------------------------------
105 # Tests
106 # ---------------------------------------------------------------------------
107
108
109 def test_i2_external_ref_in_db_but_missing_from_storage_is_rejected(
110 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
111 ) -> None:
112 """Push must abort when a snapshot manifest references an object that is
113 missing from the local object store — even though the object ID exists in
114 the manifest (i.e. it is 'in DB' but 'missing from storage').
115
116 The guard in _push_stream raises ValueError rather than sending the push
117 and letting the server discover the missing object.
118 """
119 from muse.cli.commands.push import _push_stream
120
121 root = _bare_repo(tmp_path, monkeypatch)
122
123 # Write the commit and snapshot but deliberately skip writing the object
124 # file — so the manifest has the oid but storage has no corresponding file.
125 commit = _make_commit(root, "alpha", write_objects=False)
126 (heads_dir(root) / "main").write_text(commit.commit_id)
127
128 transport = _mock_transport(result_head=commit.commit_id)
129
130 with pytest.raises(ValueError, match="missing from the local store"):
131 _push_stream(
132 transport,
133 _REMOTE_URL,
134 None,
135 root,
136 commit.commit_id,
137 [],
138 "main",
139 False,
140 )
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 121 days ago