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