gabriel / muse public
test_cmd_stress.py python
458 lines 16.0 KB
Raw
sha256:c5131d76c6eada02939111fda4aa8e51b0c1456b9983727cfd6be101916de14e merge: pull local/dev — resolve trivial _EXT_MAP symbol con… Sonnet 4.6 patch 12 days ago
1 """Stress and scale tests for Muse commands.
2
3 These tests exercise commands at a scale that would reveal
4 O(n²) performance regressions, memory leaks, and missing edge-case
5 handling. Every test in this module is designed to complete in under
6 10 seconds on a modern laptop when running from an in-memory temp
7 directory — if any test consistently takes longer, it signals a
8 performance regression worth investigating.
9
10 Scenarios:
11 - commit-graph BFS on a 500-commit linear history
12 - merge-base on a 300-deep dag (shared ancestor at the root)
13 - name-rev multi-source BFS on a 200-commit diamond graph
14 - snapshot-diff on manifests with 2000 files each
15 - verify-object on 200 objects
16 - ls-files on a 2000-file snapshot
17 - for-each-ref on 100 branches
18 - show-ref on 100 branches
19 - pack-objects → unpack-objects with 100 commits and 100 objects
20 - read-commit on 200 sequential commits
21 """
22
23 from __future__ import annotations
24
25 import datetime
26 import json
27 import pathlib
28
29 from tests.cli_test_helper import CliRunner
30
31 cli = None # argparse migration — CliRunner ignores this arg
32 from muse.core.types import blob_id, fake_id
33 from muse.core.object_store import write_object
34 from muse.core.ids import hash_commit, hash_snapshot
35 from muse.core.commits import (
36 CommitRecord,
37 write_commit,
38 )
39 from muse.core.snapshots import (
40 SnapshotRecord,
41 write_snapshot,
42 )
43 from muse.core.paths import head_path, muse_dir, ref_path
44
45 runner = CliRunner()
46
47
48 # ---------------------------------------------------------------------------
49 # Helpers
50 # ---------------------------------------------------------------------------
51
52
53 def _sha_bytes(data: bytes) -> str:
54 return blob_id(data)
55
56
57 def _init_repo(path: pathlib.Path) -> pathlib.Path:
58 muse = muse_dir(path)
59 (muse / "commits").mkdir(parents=True)
60 (muse / "snapshots").mkdir(parents=True)
61 (muse / "objects").mkdir(parents=True)
62 (muse / "refs" / "heads").mkdir(parents=True)
63 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
64 (muse / "repo.json").write_text(
65 json.dumps({"repo_id": "stress-repo", "domain": "midi"}), encoding="utf-8"
66 )
67 return path
68
69
70 def _env(repo: pathlib.Path) -> Manifest:
71 return {"MUSE_REPO_ROOT": str(repo)}
72
73
74 def _snap(repo: pathlib.Path, manifest: Manifest | None = None, tag: str = "s") -> str:
75 """Write a snapshot with a real content-addressed ID and return it."""
76 m = manifest or {}
77 sid = hash_snapshot(m)
78 write_snapshot(
79 repo,
80 SnapshotRecord(
81 snapshot_id=sid,
82 manifest=m,
83 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
84 ),
85 )
86 return sid
87
88
89 def _commit_raw(
90 repo: pathlib.Path,
91 cid: str,
92 sid: str,
93 message: str,
94 branch: str = "main",
95 parent: str | None = None,
96 parent2: str | None = None,
97 ) -> str:
98 """Write a commit with a real content-addressed ID and return it.
99
100 The *cid* parameter is ignored — the real commit ID is derived from
101 *sid*, *message*, *committed_at*, and the parent IDs using the same
102 algorithm that :func:`muse.core.ids.hash_commit` uses.
103 """
104 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
105 parent_ids = [p for p in (parent, parent2) if p is not None]
106 real_cid = hash_commit(
107 parent_ids=parent_ids,
108 snapshot_id=sid,
109 message=message,
110 committed_at_iso=committed_at.isoformat(),
111 author="stress-tester",
112 )
113 write_commit(
114 repo,
115 CommitRecord(
116 commit_id=real_cid,
117 branch=branch,
118 snapshot_id=sid,
119 message=message,
120 committed_at=committed_at,
121 author="stress-tester",
122 parent_commit_id=parent,
123 parent2_commit_id=parent2,
124 ),
125 )
126 return real_cid
127
128
129 def _set_branch(repo: pathlib.Path, branch: str, cid: str) -> None:
130 ref = ref_path(repo, branch)
131 ref.parent.mkdir(parents=True, exist_ok=True)
132 ref.write_text(cid, encoding="utf-8")
133
134
135 def _linear_chain(repo: pathlib.Path, n: int, sid: str, branch: str = "main") -> list[str]:
136 """Build a linear chain of n commits. Returns real commit IDs root→tip."""
137 cids: list[str] = []
138 parent: str | None = None
139 for i in range(n):
140 real_cid = _commit_raw(repo, "", sid, f"commit {i}", branch=branch, parent=parent)
141 cids.append(real_cid)
142 parent = real_cid
143 _set_branch(repo, branch, cids[-1])
144 return cids
145
146
147 def _obj(repo: pathlib.Path, tag: str) -> str:
148 content = tag.encode()
149 oid = _sha_bytes(content)
150 write_object(repo, oid, content)
151 return oid
152
153
154 # ---------------------------------------------------------------------------
155 # Stress: commit-graph
156 # ---------------------------------------------------------------------------
157
158
159 class TestCommitGraphStress:
160 def test_500_commit_linear_chain_full_traversal(self, tmp_path: pathlib.Path) -> None:
161 repo = _init_repo(tmp_path)
162 sid = _snap(repo)
163 cids = _linear_chain(repo, 500, sid)
164 result = runner.invoke(cli, ["commit-graph", "--json"], env=_env(repo))
165 assert result.exit_code == 0, result.output
166 data = json.loads(result.stdout)
167 assert data["count"] == 500
168 assert data["truncated"] is False
169
170 def test_500_commit_chain_stop_at_midpoint(self, tmp_path: pathlib.Path) -> None:
171 repo = _init_repo(tmp_path)
172 sid = _snap(repo)
173 cids = _linear_chain(repo, 500, sid)
174 result = runner.invoke(
175 cli,
176 ["commit-graph", "--json", "--tip", cids[499], "--stop-at", cids[249]],
177 env=_env(repo),
178 )
179 assert result.exit_code == 0
180 data = json.loads(result.stdout)
181 assert data["count"] == 250
182
183 def test_count_flag_on_500_commits(self, tmp_path: pathlib.Path) -> None:
184 repo = _init_repo(tmp_path)
185 sid = _snap(repo)
186 _linear_chain(repo, 500, sid)
187 result = runner.invoke(cli, ["commit-graph", "--count", "--json"], env=_env(repo))
188 assert result.exit_code == 0
189 data = json.loads(result.stdout)
190 assert data["count"] == 500
191 assert "commits" not in data # --count suppresses node list
192
193
194 # ---------------------------------------------------------------------------
195 # Stress: merge-base
196 # ---------------------------------------------------------------------------
197
198
199 class TestMergeBaseStress:
200 def test_merge_base_300_deep_shared_root(self, tmp_path: pathlib.Path) -> None:
201 repo = _init_repo(tmp_path)
202 sid = _snap(repo)
203
204 # Shared root
205 root_cid = _commit_raw(repo, "", sid, "root")
206
207 # Two 150-commit chains from the same root
208 main_chain = [root_cid]
209 feat_chain = [root_cid]
210 for i in range(150):
211 mc = _commit_raw(repo, "", sid, f"main-{i}", branch="main", parent=main_chain[-1])
212 main_chain.append(mc)
213 fc = _commit_raw(repo, "", sid, f"feat-{i}", branch="feat", parent=feat_chain[-1])
214 feat_chain.append(fc)
215
216 _set_branch(repo, "main", main_chain[-1])
217 _set_branch(repo, "feat", feat_chain[-1])
218 (head_path(repo)).write_text("ref: refs/heads/main", encoding="utf-8")
219
220 result = runner.invoke(
221 cli, ["merge-base", "--json", "main", "feat"], env=_env(repo)
222 )
223 assert result.exit_code == 0
224 data = json.loads(result.stdout)
225 assert data["merge_base"] == root_cid
226
227
228 # ---------------------------------------------------------------------------
229 # Stress: name-rev
230 # ---------------------------------------------------------------------------
231
232
233 class TestNameRevStress:
234 def test_name_rev_200_commit_chain_all_named(self, tmp_path: pathlib.Path) -> None:
235 repo = _init_repo(tmp_path)
236 sid = _snap(repo)
237 cids = _linear_chain(repo, 200, sid)
238
239 result = runner.invoke(cli, ["name-rev", "--json", *cids], env=_env(repo))
240 assert result.exit_code == 0
241 data = json.loads(result.stdout)
242 assert len(data["results"]) == 200
243 for entry in data["results"]:
244 assert not entry["undefined"]
245
246 def test_name_rev_tip_has_no_tilde_suffix(self, tmp_path: pathlib.Path) -> None:
247 """distance=0 means the tip is the branch tip itself; name is bare branch name."""
248 repo = _init_repo(tmp_path)
249 sid = _snap(repo)
250 cids = _linear_chain(repo, 10, sid)
251 tip = cids[-1]
252
253 result = runner.invoke(cli, ["name-rev", "--json", tip], env=_env(repo))
254 assert result.exit_code == 0
255 entry = json.loads(result.stdout)["results"][0]
256 # name-rev emits "<branch>" (no ~0) for the exact branch tip.
257 assert entry["name"] == "main"
258 assert entry["distance"] == 0
259
260
261 # ---------------------------------------------------------------------------
262 # Stress: snapshot-diff
263 # ---------------------------------------------------------------------------
264
265
266 class TestSnapshotDiffStress:
267 def test_diff_2000_file_manifests(self, tmp_path: pathlib.Path) -> None:
268 repo = _init_repo(tmp_path)
269 oid = fake_id("shared-blob")
270
271 # Manifest A: 2000 files
272 manifest_a = {f"track_{i:04d}.mid": oid for i in range(2000)}
273 # Manifest B: same 2000 files but first 200 have new IDs (modified)
274 new_oid = fake_id("new-blob")
275 manifest_b = {f"track_{i:04d}.mid": (new_oid if i < 200 else oid) for i in range(2000)}
276
277 sid_a = hash_snapshot(manifest_a)
278 sid_b = hash_snapshot(manifest_b)
279 write_snapshot(
280 repo,
281 SnapshotRecord(
282 snapshot_id=sid_a,
283 manifest=manifest_a,
284 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
285 ),
286 )
287 write_snapshot(
288 repo,
289 SnapshotRecord(
290 snapshot_id=sid_b,
291 manifest=manifest_b,
292 created_at=datetime.datetime(2026, 1, 2, tzinfo=datetime.timezone.utc),
293 ),
294 )
295
296 result = runner.invoke(cli, ["snapshot-diff", "--json", sid_a, sid_b], env=_env(repo))
297 assert result.exit_code == 0
298 data = json.loads(result.stdout)
299 assert data["total_changes"] == 200
300 assert len(data["modified"]) == 200
301 assert data["added"] == []
302 assert data["deleted"] == []
303
304
305 # ---------------------------------------------------------------------------
306 # Stress: verify-object
307 # ---------------------------------------------------------------------------
308
309
310 class TestVerifyObjectStress:
311 def test_200_objects_all_verified(self, tmp_path: pathlib.Path) -> None:
312 repo = _init_repo(tmp_path)
313 oids = [_obj(repo, f"stress-obj-{i}") for i in range(200)]
314 result = runner.invoke(cli, ["verify-object", "--json", *oids], env=_env(repo))
315 assert result.exit_code == 0
316 data = json.loads(result.stdout)
317 assert data["all_ok"] is True
318 assert data["checked"] == 200
319 assert data["failed"] == 0
320
321 def test_verify_1mib_object_no_crash(self, tmp_path: pathlib.Path) -> None:
322 repo = _init_repo(tmp_path)
323 content = b"Z" * (1024 * 1024)
324 oid = _sha_bytes(content)
325 write_object(repo, oid, content)
326 result = runner.invoke(cli, ["verify-object", "--json", oid], env=_env(repo))
327 assert result.exit_code == 0
328 assert json.loads(result.stdout)["all_ok"] is True
329
330
331 # ---------------------------------------------------------------------------
332 # Stress: ls-files
333 # ---------------------------------------------------------------------------
334
335
336 class TestLsFilesStress:
337 def test_ls_files_2000_file_snapshot(self, tmp_path: pathlib.Path) -> None:
338 repo = _init_repo(tmp_path)
339 oid = fake_id("common-oid")
340 manifest = {f"track_{i:04d}.mid": oid for i in range(2000)}
341 sid = _snap(repo, manifest, "big")
342 cid = _commit_raw(repo, "", sid, "big manifest", branch="main")
343 _set_branch(repo, "main", cid)
344
345 result = runner.invoke(cli, ["ls-files", "--json"], env=_env(repo))
346 assert result.exit_code == 0
347 data = json.loads(result.stdout)
348 assert data["file_count"] == 2000
349
350
351 # ---------------------------------------------------------------------------
352 # Stress: for-each-ref and show-ref
353 # ---------------------------------------------------------------------------
354
355
356 class TestRefCommandsStress:
357 def _build_100_branches(self, repo: pathlib.Path) -> None:
358 sid = _snap(repo, tag="multi-branch")
359 for i in range(100):
360 branch = f"feature-{i:03d}"
361 cid = _commit_raw(repo, "", sid, f"tip of {branch}", branch=branch)
362 _set_branch(repo, branch, cid)
363
364 def test_for_each_ref_100_branches(self, tmp_path: pathlib.Path) -> None:
365 repo = _init_repo(tmp_path)
366 self._build_100_branches(repo)
367 result = runner.invoke(cli, ["for-each-ref", "--json"], env=_env(repo))
368 assert result.exit_code == 0
369 data = json.loads(result.stdout)
370 assert len(data["refs"]) == 100
371
372 def test_show_ref_100_branches(self, tmp_path: pathlib.Path) -> None:
373 repo = _init_repo(tmp_path)
374 self._build_100_branches(repo)
375 result = runner.invoke(cli, ["show-ref", "--json"], env=_env(repo))
376 assert result.exit_code == 0
377 data = json.loads(result.stdout)
378 assert data["count"] == 100
379
380 def test_for_each_ref_pattern_filter_on_100(self, tmp_path: pathlib.Path) -> None:
381 repo = _init_repo(tmp_path)
382 self._build_100_branches(repo)
383 result = runner.invoke(
384 cli,
385 ["for-each-ref", "--json", "--pattern", "refs/heads/feature-00*"],
386 env=_env(repo),
387 )
388 assert result.exit_code == 0
389 data = json.loads(result.stdout)
390 # feature-000 through feature-009 = 10 branches
391 assert len(data["refs"]) == 10
392
393
394 # ---------------------------------------------------------------------------
395 # Stress: pack-objects → unpack-objects
396 # ---------------------------------------------------------------------------
397
398
399 class TestPackUnpackStress:
400 def test_100_commit_100_object_round_trip(self, tmp_path: pathlib.Path) -> None:
401 from muse.core.object_store import has_object
402 from muse.core.commits import read_commit
403
404 src = _init_repo(tmp_path / "src")
405 dst = _init_repo(tmp_path / "dst")
406
407 # Build 100 objects
408 oids = [_obj(src, f"blob-{i}") for i in range(100)]
409 manifest = {f"f{i}.mid": oids[i] for i in range(100)}
410 sid = _snap(src, manifest, "big-pack")
411
412 # Build 100-commit linear chain referencing that snapshot
413 parent: str | None = None
414 cids: list[str] = []
415 for i in range(100):
416 cid = _commit_raw(src, "", sid, f"pack-{i}", parent=parent)
417 cids.append(cid)
418 parent = cid
419 _set_branch(src, "main", cids[-1])
420
421 # Pack tip → unpack into dst
422 pack_result = runner.invoke(
423 cli, ["pack-objects", cids[-1]], env=_env(src)
424 )
425 assert pack_result.exit_code == 0
426
427 unpack_result = runner.invoke(
428 cli,
429 ["unpack-objects", "--json"],
430 input=pack_result.stdout_bytes,
431 env=_env(dst),
432 )
433 assert unpack_result.exit_code == 0
434 counts = json.loads(unpack_result.stdout)
435 assert counts["commits_written"] == 100
436 assert counts["blobs_written"] == 100
437
438 for cid in cids:
439 assert read_commit(dst, cid) is not None
440 for oid in oids:
441 assert has_object(dst, oid)
442
443
444 # ---------------------------------------------------------------------------
445 # Stress: read-commit sequential
446 # ---------------------------------------------------------------------------
447
448
449 class TestReadCommitStress:
450 def test_200_commits_all_readable(self, tmp_path: pathlib.Path) -> None:
451 repo = _init_repo(tmp_path)
452 sid = _snap(repo)
453 cids = _linear_chain(repo, 200, sid)
454 for cid in cids:
455 result = runner.invoke(cli, ["read-commit", "--json", cid], env=_env(repo))
456 assert result.exit_code == 0
457 data = json.loads(result.stdout)
458 assert data["commit_id"] == cid
File History 6 commits
sha256:c5131d76c6eada02939111fda4aa8e51b0c1456b9983727cfd6be101916de14e merge: pull local/dev — resolve trivial _EXT_MAP symbol con… Sonnet 4.6 patch 12 days ago
sha256:9c33d61749fff814c5226d5386aa2af7064c2c02788594a25fdd709358132eea fix: _PROPOSAL_PREFIX_RESOLVE_LIMIT 200 → 100 to match hub … Sonnet 4.6 19 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:0313c134f0ef4518a9c3a0ec359ffdc42546dc720010730374edfe0857caf7ef rename: delta_add → delta_upsert across wire format, source… Sonnet 4.6 minor 23 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago