gabriel / muse public
test_cmd_bundle.py python
266 lines 9.0 KB
dec4604a feat(mwp): replace JSON+base64 wire protocol with MWP binary msgpack Gabriel Cardona <gabriel@tellurstori.com> 13h 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_snapshot_id
21 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
22
23 runner = CliRunner()
24
25 _REPO_ID = "bundle-test"
26
27
28 # ---------------------------------------------------------------------------
29 # Helpers
30 # ---------------------------------------------------------------------------
31
32
33 def _sha(data: bytes) -> str:
34 return hashlib.sha256(data).hexdigest()
35
36
37 def _init_repo(path: pathlib.Path, repo_id: str = _REPO_ID) -> pathlib.Path:
38 muse = path / ".muse"
39 for d in ("commits", "snapshots", "objects", "refs/heads"):
40 (muse / d).mkdir(parents=True, exist_ok=True)
41 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
42 (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) -> dict[str, str]:
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 = _sha(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 commit_id = _sha(f"{_counter}:{snap_id}:{committed_at.isoformat()}".encode())
71 write_commit(root, CommitRecord(
72 commit_id=commit_id,
73 repo_id=_REPO_ID,
74 branch=branch,
75 snapshot_id=snap_id,
76 message=f"commit {_counter}",
77 committed_at=committed_at,
78 parent_commit_id=parent_id,
79 ))
80 (root / ".muse" / "refs" / "heads" / branch).write_text(commit_id, encoding="utf-8")
81 return commit_id
82
83
84 # ---------------------------------------------------------------------------
85 # Unit: help
86 # ---------------------------------------------------------------------------
87
88
89 def test_bundle_help() -> None:
90 result = runner.invoke(cli, ["bundle", "--help"])
91 assert result.exit_code == 0
92
93
94 def test_bundle_create_help() -> None:
95 result = runner.invoke(cli, ["bundle", "create", "--help"])
96 assert result.exit_code == 0
97
98
99 # ---------------------------------------------------------------------------
100 # Unit: create
101 # ---------------------------------------------------------------------------
102
103
104 def test_bundle_create_basic(tmp_path: pathlib.Path) -> None:
105 _init_repo(tmp_path)
106 _make_commit(tmp_path, content=b"first")
107 out = tmp_path / "out.bundle"
108 result = runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
109 assert result.exit_code == 0
110 assert out.exists()
111 data = msgpack.unpackb(out.read_bytes(), raw=False)
112 assert "commits" in data
113 assert len(data["commits"]) >= 1
114
115
116 def test_bundle_create_no_commits(tmp_path: pathlib.Path) -> None:
117 _init_repo(tmp_path)
118 out = tmp_path / "empty.bundle"
119 result = runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
120 assert result.exit_code != 0 # no commits to bundle
121
122
123 # ---------------------------------------------------------------------------
124 # Unit: verify clean
125 # ---------------------------------------------------------------------------
126
127
128 def test_bundle_verify_clean(tmp_path: pathlib.Path) -> None:
129 _init_repo(tmp_path)
130 _make_commit(tmp_path, content=b"verify me")
131 out = tmp_path / "clean.bundle"
132 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
133 result = runner.invoke(cli, ["bundle", "verify", str(out)], env=_env(tmp_path))
134 assert result.exit_code == 0
135 assert "clean" in result.output.lower()
136
137
138 def test_bundle_verify_corrupt(tmp_path: pathlib.Path) -> None:
139 _init_repo(tmp_path)
140 _make_commit(tmp_path, content=b"to corrupt")
141 out = tmp_path / "corrupt.bundle"
142 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
143
144 # Tamper with an object's content bytes.
145 raw = msgpack.unpackb(out.read_bytes(), raw=False)
146 if raw.get("objects"):
147 raw["objects"][0]["content"] = b"tampered!"
148 out.write_bytes(msgpack.packb(raw, use_bin_type=True))
149
150 result = runner.invoke(cli, ["bundle", "verify", str(out)], env=_env(tmp_path))
151 assert result.exit_code != 0
152 assert "mismatch" in result.output.lower() or "failure" in result.output.lower()
153
154
155 def test_bundle_verify_json(tmp_path: pathlib.Path) -> None:
156 _init_repo(tmp_path)
157 _make_commit(tmp_path, content=b"json verify")
158 out = tmp_path / "jv.bundle"
159 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
160 result = runner.invoke(cli, ["bundle", "verify", str(out), "--format", "json"], env=_env(tmp_path))
161 assert result.exit_code == 0
162 data = json.loads(result.output)
163 assert data["all_ok"] is True
164
165
166 def test_bundle_verify_quiet_clean(tmp_path: pathlib.Path) -> None:
167 _init_repo(tmp_path)
168 _make_commit(tmp_path, content=b"quiet clean")
169 out = tmp_path / "q.bundle"
170 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
171 result = runner.invoke(cli, ["bundle", "verify", str(out), "-q"], env=_env(tmp_path))
172 assert result.exit_code == 0
173
174
175 # ---------------------------------------------------------------------------
176 # Unit: unbundle
177 # ---------------------------------------------------------------------------
178
179
180 def test_bundle_unbundle_writes_objects(tmp_path: pathlib.Path) -> None:
181 src = tmp_path / "src"
182 dst = tmp_path / "dst"
183 src.mkdir()
184 dst.mkdir()
185 _init_repo(src)
186 _init_repo(dst, repo_id="dst-repo")
187 _make_commit(src, content=b"unbundle me")
188
189 out = tmp_path / "unbundle_test.bundle"
190 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(src))
191
192 result = runner.invoke(cli, ["bundle", "unbundle", str(out)], env=_env(dst))
193 assert result.exit_code == 0
194 assert "unpacked" in result.output.lower()
195
196
197 # ---------------------------------------------------------------------------
198 # Unit: list-heads
199 # ---------------------------------------------------------------------------
200
201
202 def test_bundle_list_heads_text(tmp_path: pathlib.Path) -> None:
203 _init_repo(tmp_path)
204 _make_commit(tmp_path, content=b"heads test")
205 out = tmp_path / "heads.bundle"
206 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
207 result = runner.invoke(cli, ["bundle", "list-heads", str(out)], env=_env(tmp_path))
208 assert result.exit_code == 0
209
210
211 def test_bundle_list_heads_json(tmp_path: pathlib.Path) -> None:
212 _init_repo(tmp_path)
213 _make_commit(tmp_path, content=b"json heads")
214 out = tmp_path / "jheads.bundle"
215 runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
216 result = runner.invoke(cli, ["bundle", "list-heads", str(out), "--format", "json"], env=_env(tmp_path))
217 assert result.exit_code == 0
218 json.loads(result.output) # valid JSON
219
220
221 # ---------------------------------------------------------------------------
222 # Integration: full round-trip
223 # ---------------------------------------------------------------------------
224
225
226 def test_bundle_round_trip(tmp_path: pathlib.Path) -> None:
227 """Create a bundle from a source repo, unbundle into a clean target."""
228 src = tmp_path / "src"
229 dst = tmp_path / "dst"
230 src.mkdir()
231 dst.mkdir()
232 _init_repo(src)
233 _init_repo(dst, repo_id="dst-rt")
234
235 prev: str | None = None
236 for i in range(5):
237 prev = _make_commit(src, parent_id=prev, content=f"rt-{i}".encode())
238
239 out = tmp_path / "rt.bundle"
240 create_result = runner.invoke(cli, ["bundle", "create", str(out)], env=_env(src))
241 assert create_result.exit_code == 0
242
243 unbundle_result = runner.invoke(cli, ["bundle", "unbundle", str(out)], env=_env(dst))
244 assert unbundle_result.exit_code == 0
245
246
247 # ---------------------------------------------------------------------------
248 # Stress: 50-commit bundle
249 # ---------------------------------------------------------------------------
250
251
252 def test_bundle_stress_50_commits(tmp_path: pathlib.Path) -> None:
253 _init_repo(tmp_path)
254 prev: str | None = None
255 for i in range(50):
256 prev = _make_commit(tmp_path, parent_id=prev, content=f"stress-{i}".encode())
257
258 out = tmp_path / "stress.bundle"
259 result = runner.invoke(cli, ["bundle", "create", str(out)], env=_env(tmp_path))
260 assert result.exit_code == 0
261
262 raw = msgpack.unpackb(out.read_bytes(), raw=False)
263 assert len(raw.get("commits", [])) == 50
264
265 verify_result = runner.invoke(cli, ["bundle", "verify", str(out), "-q"], env=_env(tmp_path))
266 assert verify_result.exit_code == 0