gabriel / muse public
test_cmd_bundle.py python
266 lines 9.0 KB
1ba7f7b1 feat(porcelain): implement 9 gap-fill porcelain commands with full test… Gabriel Cardona <gabriel@tellurstori.com> 2d 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 base64
10 import datetime
11 import hashlib
12 import json
13 import pathlib
14
15 import pytest
16 from typer.testing import CliRunner
17
18 from muse.cli.app import cli
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 = json.loads(out.read_text(encoding="utf-8"))
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_b64.
145 raw = json.loads(out.read_text(encoding="utf-8"))
146 if raw.get("objects"):
147 raw["objects"][0]["content_b64"] = base64.b64encode(b"tampered!").decode("ascii")
148 out.write_text(json.dumps(raw), encoding="utf-8")
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 = json.loads(out.read_text(encoding="utf-8"))
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