gabriel / muse public
test_clone_partial.py python
184 lines 6.8 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 121 days ago
1 """TDD — clone with corrupt objects must exit PARTIAL (exit code 3), not INTERNAL_ERROR.
2
3 C1 When fetch_presign_or_stream delivers objects and some fail the integrity
4 check, clone must:
5 - write the objects that are clean
6 - skip the corrupt ones with a warning
7 - exit with ExitCode.PARTIAL (3), not ExitCode.INTERNAL_ERROR (3 currently
8 mis-mapped to INTERNAL_ERROR)
9 - document this in the help text and ExitCode enum
10
11 C2 The --json envelope must include a "skipped_objects" count when > 0.
12 """
13 from __future__ import annotations
14
15 import json
16 import pathlib
17 import unittest.mock
18
19 import pytest
20
21 from muse.core.errors import ExitCode
22 from muse.core.pack import MPackBundle, ObjectPayload, RemoteInfo
23 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
24 from muse.core.types import blob_id, long_id
25 from tests.cli_test_helper import CliRunner
26
27 runner = CliRunner()
28 cli = None
29
30
31 def _make_remote_info(repo_id: str = "repo-partial") -> RemoteInfo:
32 return RemoteInfo(
33 repo_id=repo_id,
34 domain="code",
35 default_branch="main",
36 branch_heads={"main": long_id("c" * 64)},
37 )
38
39
40 def _make_bundle_with_corrupt_object() -> tuple[MPackBundle, str, str]:
41 """Return (bundle, good_oid, corrupt_oid).
42
43 The bundle contains two objects:
44 - good_oid: content matches its ID
45 - corrupt_oid: content is wrong bytes (sha256 mismatch)
46 """
47 committed_at = "2026-01-01T00:00:00+00:00"
48
49 good_content = b"good content"
50 good_oid = blob_id(good_content)
51
52 corrupt_content = b"wrong bytes"
53 # Use an OID that does NOT match corrupt_content
54 corrupt_oid = blob_id(b"the real content that should be here")
55
56 snap_id = compute_snapshot_id({"good.txt": good_oid, "bad.txt": corrupt_oid})
57 cid = compute_commit_id(
58 parent_ids=[],
59 snapshot_id=snap_id,
60 message="test commit",
61 committed_at_iso=committed_at,
62 author="gabriel",
63 )
64 branch_heads = {"main": cid}
65
66 bundle = MPackBundle(
67 commits=[{
68 "commit_id": cid,
69 "repo_id": "repo-partial",
70 "branch": "main",
71 "snapshot_id": snap_id,
72 "message": "test commit",
73 "committed_at": committed_at,
74 "parent_commit_id": None,
75 "parent2_commit_id": None,
76 "author": "gabriel",
77 "metadata": {},
78 "structured_delta": None,
79 "sem_ver_bump": "none",
80 "breaking_changes": [],
81 "agent_id": "",
82 "model_id": "",
83 "toolchain_id": "",
84 "prompt_hash": "",
85 "signature": "",
86 "signer_key_id": "",
87 "reviewed_by": [],
88 "test_runs": 0,
89 }],
90 snapshots=[{
91 "snapshot_id": snap_id,
92 "manifest": {"good.txt": good_oid, "bad.txt": corrupt_oid},
93 "created_at": committed_at,
94 }],
95 objects=[
96 ObjectPayload(object_id=good_oid, content=good_content),
97 ObjectPayload(object_id=corrupt_oid, content=corrupt_content),
98 ],
99 branch_heads=branch_heads,
100 )
101 return bundle, good_oid, corrupt_oid
102
103
104 def _mock_transport(info: RemoteInfo, bundle: MPackBundle) -> unittest.mock.MagicMock:
105 mock = unittest.mock.MagicMock()
106 # Use the bundle's real content-addressed branch heads so apply_mpack and
107 # _restore_working_tree can find the commit after the fetch.
108 real_heads = bundle["branch_heads"]
109 patched_info = {**info, "branch_heads": real_heads}
110 mock.fetch_remote_info.return_value = patched_info
111 fetch_result = {
112 "repo_id": info["repo_id"],
113 "domain": info["domain"],
114 "default_branch": info["default_branch"],
115 "branch_heads": real_heads,
116 "commits": bundle["commits"],
117 "snapshots": bundle["snapshots"],
118 "objects_received": len(bundle["objects"]),
119 "shallow_commits": [],
120 }
121
122 def _fake_fetch_presign_or_stream(url, signing, *, want, have, on_object=None, **kw):
123 if on_object is not None:
124 for obj in bundle["objects"]:
125 on_object(obj)
126 return fetch_result
127
128 mock.fetch_stream.return_value = fetch_result
129 mock.fetch_presign_or_stream.side_effect = _fake_fetch_presign_or_stream
130 return mock
131
132
133 class TestClonePartial:
134 def test_exit_code_is_partial_when_objects_skipped(
135 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
136 ) -> None:
137 """Clone with a corrupt object must exit PARTIAL, not INTERNAL_ERROR."""
138 monkeypatch.chdir(tmp_path)
139 info = _make_remote_info()
140 bundle, good_oid, corrupt_oid = _make_bundle_with_corrupt_object()
141 mock = _mock_transport(info, bundle)
142
143 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
144 result = runner.invoke(cli, ["clone", "https://hub.example.com/repo-partial", "cloned"])
145
146 assert result.exit_code == ExitCode.PARTIAL
147
148 def test_good_object_is_written_despite_corrupt_sibling(
149 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
150 ) -> None:
151 """The clean object must be written even when another object is corrupt."""
152 monkeypatch.chdir(tmp_path)
153 info = _make_remote_info()
154 bundle, good_oid, corrupt_oid = _make_bundle_with_corrupt_object()
155 mock = _mock_transport(info, bundle)
156
157 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
158 runner.invoke(cli, ["clone", "https://hub.example.com/repo-partial", "cloned"])
159
160 from muse.core.object_store import object_path
161 from muse.core.paths import server_objects_dir
162 cloned = tmp_path / "cloned"
163 good_path = object_path(cloned, good_oid)
164 corrupt_path = object_path(cloned, corrupt_oid)
165 assert good_path.exists(), "clean object must be written"
166 assert not corrupt_path.exists(), "corrupt object must not be written"
167
168 def test_json_envelope_includes_skipped_objects_count(
169 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
170 ) -> None:
171 """--json output must include skipped_objects count."""
172 monkeypatch.chdir(tmp_path)
173 info = _make_remote_info()
174 bundle, good_oid, corrupt_oid = _make_bundle_with_corrupt_object()
175 mock = _mock_transport(info, bundle)
176
177 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
178 result = runner.invoke(cli, ["clone", "https://hub.example.com/repo-partial", "cloned", "--json"])
179
180 lines = [l for l in result.output.strip().splitlines() if l.startswith("{")]
181 assert lines, "expected JSON output"
182 d = json.loads(lines[0])
183 assert "skipped_objects" in d, "JSON envelope must include skipped_objects"
184 assert d["skipped_objects"] == 1
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 121 days ago