gabriel / muse public
test_cmd_bundle.py python
271 lines 9.2 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 120 days ago
1 """Tests for ``muse bundle`` subcommands.
2
3 Covers: create (default/have prune), unbundle (ref update), verify (clean/corrupt),
4 list-heads, round-trip, stress: 50-commit bundle.
5 """
6
7 from __future__ import annotations
8
9 import datetime
10 import hashlib
11 import json
12 import pathlib
13
14 import msgpack
15 import pytest
16 from tests.cli_test_helper import CliRunner
17
18 cli = None # argparse migration — CliRunner ignores this arg
19 from muse.core.object_store import write_object
20 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
21 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
22 from muse.core.types import Manifest, long_id, blob_id
23 from muse.core.paths import muse_dir, ref_path
24
25 runner = CliRunner()
26
27 _REPO_ID = "bundle-test"
28
29
30 # ---------------------------------------------------------------------------
31 # Helpers
32 # ---------------------------------------------------------------------------
33
34
35
36
37 def _init_repo(path: pathlib.Path, repo_id: str = _REPO_ID) -> pathlib.Path:
38 dot_muse = muse_dir(path)
39 for d in ("commits", "snapshots", "objects", "refs/heads"):
40 (dot_muse / d).mkdir(parents=True, exist_ok=True)
41 (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
42 (dot_muse / "repo.json").write_text(
43 json.dumps({"repo_id": repo_id, "domain": "midi"}), encoding="utf-8"
44 )
45 return path
46
47
48 def _env(repo: pathlib.Path) -> Manifest:
49 return {"MUSE_REPO_ROOT": str(repo)}
50
51
52 _counter = 0
53
54
55 def _make_commit(
56 root: pathlib.Path,
57 parent_id: str | None = None,
58 content: bytes = b"data",
59 branch: str = "main",
60 ) -> str:
61 global _counter
62 _counter += 1
63 c = content + str(_counter).encode()
64 obj_id = long_id(blob_id(c))
65 write_object(root, obj_id, c)
66 manifest = {f"f_{_counter}.txt": obj_id}
67 snap_id = compute_snapshot_id(manifest)
68 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
69 committed_at = datetime.datetime.now(datetime.timezone.utc)
70 parent_ids = [parent_id] if parent_id else []
71 commit_id = compute_commit_id( parent_ids=parent_ids,
72 snapshot_id=snap_id,
73 message=f"commit {_counter}",
74 committed_at_iso=committed_at.isoformat(),
75 )
76 write_commit(root, CommitRecord(
77 commit_id=commit_id,
78 repo_id="test-repo",
79 branch=branch,
80 snapshot_id=snap_id,
81 message=f"commit {_counter}",
82 committed_at=committed_at,
83 parent_commit_id=parent_id,
84 ))
85 (ref_path(root, branch)).write_text(commit_id, encoding="utf-8")
86 return commit_id
87
88
89 # ---------------------------------------------------------------------------
90 # Unit: help
91 # ---------------------------------------------------------------------------
92
93
94 def test_bundle_help() -> None:
95 result = runner.invoke(cli, ["bundle", "--help"])
96 assert result.exit_code == 0
97
98
99 def test_bundle_create_help() -> None:
100 result = runner.invoke(cli, ["bundle", "create", "--help"])
101 assert result.exit_code == 0
102
103
104 # ---------------------------------------------------------------------------
105 # Unit: create
106 # ---------------------------------------------------------------------------
107
108
109 def test_bundle_create_basic(tmp_path: pathlib.Path) -> None:
110 _init_repo(tmp_path)
111 _make_commit(tmp_path, content=b"first")
112 out = tmp_path / "out.bundle"
113 result = runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
114 assert result.exit_code == 0
115 assert out.exists()
116 data = msgpack.unpackb(out.read_bytes(), raw=False)
117 assert "commits" in data
118 assert len(data["commits"]) >= 1
119
120
121 def test_bundle_create_no_commits(tmp_path: pathlib.Path) -> None:
122 _init_repo(tmp_path)
123 out = tmp_path / "empty.bundle"
124 result = runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
125 assert result.exit_code != 0 # no commits to bundle
126
127
128 # ---------------------------------------------------------------------------
129 # Unit: verify clean
130 # ---------------------------------------------------------------------------
131
132
133 def test_bundle_verify_clean(tmp_path: pathlib.Path) -> None:
134 _init_repo(tmp_path)
135 _make_commit(tmp_path, content=b"verify me")
136 out = tmp_path / "clean.bundle"
137 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
138 result = runner.invoke(cli, ["bundle", "verify", str(out)], env=_env(tmp_path))
139 assert result.exit_code == 0
140 assert "clean" in result.output.lower()
141
142
143 def test_bundle_verify_corrupt(tmp_path: pathlib.Path) -> None:
144 _init_repo(tmp_path)
145 _make_commit(tmp_path, content=b"to corrupt")
146 out = tmp_path / "corrupt.bundle"
147 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
148
149 # Tamper with an object's content bytes.
150 raw = msgpack.unpackb(out.read_bytes(), raw=False)
151 if raw.get("objects"):
152 raw["objects"][0]["content"] = b"tampered!"
153 out.write_bytes(msgpack.packb(raw, use_bin_type=True))
154
155 result = runner.invoke(cli, ["bundle", "verify", str(out)], env=_env(tmp_path))
156 assert result.exit_code != 0
157 assert "mismatch" in result.output.lower() or "failure" in result.output.lower()
158
159
160 def test_bundle_verify_json(tmp_path: pathlib.Path) -> None:
161 _init_repo(tmp_path)
162 _make_commit(tmp_path, content=b"json verify")
163 out = tmp_path / "jv.bundle"
164 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
165 result = runner.invoke(cli, ["bundle", "verify", str(out), "--json"], env=_env(tmp_path))
166 assert result.exit_code == 0
167 data = json.loads(result.output)
168 assert data["all_ok"] is True
169
170
171 def test_bundle_verify_quiet_clean(tmp_path: pathlib.Path) -> None:
172 _init_repo(tmp_path)
173 _make_commit(tmp_path, content=b"quiet clean")
174 out = tmp_path / "q.bundle"
175 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
176 result = runner.invoke(cli, ["bundle", "verify", str(out), "-q"], env=_env(tmp_path))
177 assert result.exit_code == 0
178
179
180 # ---------------------------------------------------------------------------
181 # Unit: unbundle
182 # ---------------------------------------------------------------------------
183
184
185 def test_bundle_unbundle_writes_objects(tmp_path: pathlib.Path) -> None:
186 src = tmp_path / "src"
187 dst = tmp_path / "dst"
188 src.mkdir()
189 dst.mkdir()
190 _init_repo(src)
191 _init_repo(dst, repo_id="dst-repo")
192 _make_commit(src, content=b"unbundle me")
193
194 out = tmp_path / "unbundle_test.bundle"
195 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(src))
196
197 result = runner.invoke(cli, ["bundle", "unbundle", str(out)], env=_env(dst))
198 assert result.exit_code == 0
199 assert "unpacked" in result.output.lower()
200
201
202 # ---------------------------------------------------------------------------
203 # Unit: list-heads
204 # ---------------------------------------------------------------------------
205
206
207 def test_bundle_list_heads_text(tmp_path: pathlib.Path) -> None:
208 _init_repo(tmp_path)
209 _make_commit(tmp_path, content=b"heads test")
210 out = tmp_path / "heads.bundle"
211 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
212 result = runner.invoke(cli, ["bundle", "list-heads", str(out)], env=_env(tmp_path))
213 assert result.exit_code == 0
214
215
216 def test_bundle_list_heads_json(tmp_path: pathlib.Path) -> None:
217 _init_repo(tmp_path)
218 _make_commit(tmp_path, content=b"json heads")
219 out = tmp_path / "jheads.bundle"
220 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
221 result = runner.invoke(cli, ["bundle", "list-heads", str(out), "--json"], env=_env(tmp_path))
222 assert result.exit_code == 0
223 json.loads(result.output) # valid JSON
224
225
226 # ---------------------------------------------------------------------------
227 # Integration: full round-trip
228 # ---------------------------------------------------------------------------
229
230
231 def test_bundle_round_trip(tmp_path: pathlib.Path) -> None:
232 """Create a bundle from a source repo, unbundle into a clean target."""
233 src = tmp_path / "src"
234 dst = tmp_path / "dst"
235 src.mkdir()
236 dst.mkdir()
237 _init_repo(src)
238 _init_repo(dst, repo_id="dst-rt")
239
240 prev: str | None = None
241 for i in range(5):
242 prev = _make_commit(src, parent_id=prev, content=f"rt-{i}".encode())
243
244 out = tmp_path / "rt.bundle"
245 create_result = runner.invoke(cli, ["bundle", "create", str(out)], env=_env(src))
246 assert create_result.exit_code == 0
247
248 unbundle_result = runner.invoke(cli, ["bundle", "unbundle", str(out)], env=_env(dst))
249 assert unbundle_result.exit_code == 0
250
251
252 # ---------------------------------------------------------------------------
253 # Stress: 50-commit bundle
254 # ---------------------------------------------------------------------------
255
256
257 def test_bundle_stress_50_commits(tmp_path: pathlib.Path) -> None:
258 _init_repo(tmp_path)
259 prev: str | None = None
260 for i in range(50):
261 prev = _make_commit(tmp_path, parent_id=prev, content=f"stress-{i}".encode())
262
263 out = tmp_path / "stress.bundle"
264 result = runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
265 assert result.exit_code == 0
266
267 raw = msgpack.unpackb(out.read_bytes(), raw=False)
268 assert len(raw.get("commits", [])) == 50
269
270 verify_result = runner.invoke(cli, ["bundle", "verify", str(out), "-q"], env=_env(tmp_path))
271 assert verify_result.exit_code == 0
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 120 days ago