gabriel / muse public
test_cmd_shard.py python
896 lines 43.7 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 142 days ago
1 """Comprehensive tests for ``muse coord shard``.
2
3 Coverage matrix
4 ---------------
5 Unit
6 ~~~~
7 * _build_import_edges — language filter, missing objects, stem matching
8 * _connected_components — isolated nodes, simple chain, full graph, cycle
9 * _greedy_partition — single shard, multi-shard balance, more shards than
10 components, empty components, symbol-count weighting verified
11
12 Integration
13 ~~~~~~~~~~~
14 * Empty repo (no commits) — exits 1 with "not found" message
15 * --agents 0 — exits 1 with clean error (no traceback)
16 * --agents 257 — exits 1 with clean error
17 * --agents at boundary 1 — accepted
18 * --agents at boundary 256 — accepted
19 * --agents validation fires before require_repo (no .muse dir needed)
20 * --agents 1 with mocked snapshot — text output with "Shard plan" header
21 * --format json — valid JSON with all required schema fields
22 * --json shorthand — same as --format json
23 * --language filter — restricts file selection (language_filter kwarg)
24 * --commit REF — passed through to resolve_commit_ref
25 * No snapshot manifest — exits 0 with "(no semantic files found)"
26 * Text output — header, shard lines, cross-shard edges, elapsed
27 * JSON: full_commit_id is the complete commit ID, not 8 chars
28 * JSON: total_files and total_symbols present and correct
29 * JSON: duration_ms present and non-negative
30 * JSON: cross_shard_edges correct count
31 * JSON: no-files case still emits valid schema
32
33 Error shapes
34 ~~~~~~~~~~~~
35 * --agents out of range: JSON error has {"error": ..., "status": "bad_args"}
36 * --agents out of range: text error uses ❌ prefix on stderr
37 * commit not found: JSON error has {"error": ..., "status": "commit_not_found"}
38 * commit not found: text error uses ❌ prefix on stderr
39
40 Security
41 ~~~~~~~~
42 * --language value sanitised in text output (ANSI injection stripped)
43 * --language filter does not traverse filesystem
44 * --commit traversal ref handled gracefully (no crash)
45 * file paths in text output sanitised (ANSI stripped)
46
47 Stress
48 ~~~~~~
49 * 100-file mock snapshot partitioned into 8 shards — runs in < 2 s
50 * 500-file isolated nodes partitioned into 16 shards — runs in < 2 s
51 * 200-file dense graph (chain) into 4 shards — cross_shard_edges correct
52 * JSON output with 500 shards is a single compact line (no indent)
53
54 E2E
55 ~~~
56 * Single file → 1 shard, 0 cross-shard edges
57 * Two disconnected clusters → 2 shards, 0 cross-shard edges
58 * Connected pair split across 2 shards → cross_shard_edges ≥ 1
59 * shards_created = min(agents, components)
60 """
61
62 from __future__ import annotations
63
64 import io
65 import json
66 import pathlib
67 import sys
68 import time
69 import pytest
70 from unittest.mock import patch, MagicMock
71
72 from muse.core._types import fake_id, short_id
73 from tests.cli_test_helper import CliRunner, InvokeResult
74 from muse.cli.commands.shard import _MIN_AGENTS, _MAX_AGENTS
75
76 runner = CliRunner()
77 cli = None
78
79
80 # ── Fixtures ──────────────────────────────────────────────────────────────────
81
82
83 @pytest.fixture()
84 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
85 muse_dir = tmp_path / ".muse"
86 muse_dir.mkdir()
87 (muse_dir / "HEAD").write_text("ref: refs/heads/main\n")
88 (muse_dir / "repo.json").write_text(
89 json.dumps({"repo_id": fake_id("repo"), "name": "test-repo"})
90 )
91 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
92 return tmp_path
93
94
95 # ── Minimal commit stub ───────────────────────────────────────────────────────
96
97
98 def _make_commit_stub(commit_id: str = "a1b2c3d4e5f60000") -> MagicMock:
99 stub = MagicMock()
100 stub.commit_id = commit_id
101 return stub
102
103
104 # ── Unit: _build_import_edges ─────────────────────────────────────────────────
105
106
107 class TestBuildImportEdges:
108 def test_empty_manifest_returns_no_edges(self, repo: pathlib.Path) -> None:
109 from muse.cli.commands.shard import _build_import_edges
110 edges = _build_import_edges(repo, {}, language_filter=None)
111 assert edges == []
112
113 def test_language_filter_excludes_unmatched_files(self, repo: pathlib.Path) -> None:
114 from muse.cli.commands.shard import _build_import_edges
115 manifest = {"src/foo.py": "obj1", "src/bar.ts": "obj2"}
116 # read_object returns None → no parse → no edges; language filter prunes ts
117 with patch("muse.cli.commands.shard.read_object", return_value=None):
118 edges = _build_import_edges(repo, manifest, language_filter="Python")
119 assert edges == []
120
121 def test_missing_object_skipped_gracefully(self, repo: pathlib.Path) -> None:
122 from muse.cli.commands.shard import _build_import_edges
123 manifest = {"src/foo.py": "nonexistent-oid"}
124 with patch("muse.cli.commands.shard.read_object", return_value=None):
125 edges = _build_import_edges(repo, manifest, language_filter=None)
126 assert edges == []
127
128 def test_import_edge_built_from_parsed_symbol(self, repo: pathlib.Path) -> None:
129 from muse.cli.commands.shard import _build_import_edges
130 manifest = {"src/foo.py": "oid-foo", "src/bar.py": "oid-bar"}
131 fake_tree = {
132 "import::bar": {
133 "kind": "import",
134 "qualified_name": "import::bar",
135 "name": "bar",
136 }
137 }
138 with (
139 patch("muse.cli.commands.shard.read_object", return_value=b"dummy"),
140 patch("muse.cli.commands.shard.parse_symbols", return_value=fake_tree),
141 ):
142 edges = _build_import_edges(repo, manifest, language_filter=None)
143 # (src/foo.py, src/bar.py) or (src/bar.py, src/foo.py) edge expected
144 assert len(edges) >= 1
145 found = any(
146 ("src/foo.py" in e and "src/bar.py" in e) for e in edges
147 )
148 assert found
149
150
151 # ── Unit: _connected_components ───────────────────────────────────────────────
152
153
154 class TestConnectedComponents:
155 def test_empty_files(self) -> None:
156 from muse.cli.commands.shard import _connected_components
157 result = _connected_components([], [])
158 assert result == []
159
160 def test_single_file_no_edges(self) -> None:
161 from muse.cli.commands.shard import _connected_components
162 result = _connected_components(["a.py"], [])
163 assert len(result) == 1
164 assert result[0] == frozenset({"a.py"})
165
166 def test_two_isolated_files(self) -> None:
167 from muse.cli.commands.shard import _connected_components
168 result = _connected_components(["a.py", "b.py"], [])
169 assert len(result) == 2
170
171 def test_two_connected_files(self) -> None:
172 from muse.cli.commands.shard import _connected_components
173 result = _connected_components(["a.py", "b.py"], [("a.py", "b.py")])
174 assert len(result) == 1
175 assert result[0] == frozenset({"a.py", "b.py"})
176
177 def test_chain_of_three(self) -> None:
178 from muse.cli.commands.shard import _connected_components
179 files = ["a.py", "b.py", "c.py"]
180 edges = [("a.py", "b.py"), ("b.py", "c.py")]
181 result = _connected_components(files, edges)
182 assert len(result) == 1
183 assert result[0] == frozenset({"a.py", "b.py", "c.py"})
184
185 def test_two_separate_components(self) -> None:
186 from muse.cli.commands.shard import _connected_components
187 files = ["a.py", "b.py", "c.py", "d.py"]
188 edges = [("a.py", "b.py"), ("c.py", "d.py")]
189 result = _connected_components(files, edges)
190 assert len(result) == 2
191 sizes = sorted(len(c) for c in result)
192 assert sizes == [2, 2]
193
194
195 # ── Unit: _greedy_partition ───────────────────────────────────────────────────
196
197
198 class TestGreedyPartition:
199 def test_single_shard_all_in_one(self) -> None:
200 from muse.cli.commands.shard import _greedy_partition
201 comps = [frozenset({"a.py"}), frozenset({"b.py"})]
202 sym_counts = {"a.py": 5, "b.py": 3}
203 result = _greedy_partition(comps, sym_counts, n_shards=1)
204 assert len(result) == 1
205 assert result[0] == frozenset({"a.py", "b.py"})
206
207 def test_balanced_across_shards(self) -> None:
208 from muse.cli.commands.shard import _greedy_partition
209 comps = [frozenset({f"f{i}.py"}) for i in range(4)]
210 sym_counts = {f"f{i}.py": 10 for i in range(4)}
211 result = _greedy_partition(comps, sym_counts, n_shards=2)
212 sizes = [sum(sym_counts[f] for f in s) for s in result]
213 assert sizes[0] == sizes[1] == 20
214
215 def test_more_shards_than_components(self) -> None:
216 from muse.cli.commands.shard import _greedy_partition
217 comps = [frozenset({"a.py"})]
218 sym_counts = {"a.py": 2}
219 result = _greedy_partition(comps, sym_counts, n_shards=4)
220 # Only one shard is non-empty
221 non_empty = [s for s in result if s]
222 assert len(non_empty) == 1
223
224 def test_empty_components_produces_empty_shards(self) -> None:
225 from muse.cli.commands.shard import _greedy_partition
226 result = _greedy_partition([], {}, n_shards=3)
227 assert all(len(s) == 0 for s in result)
228
229
230 # ── Integration ───────────────────────────────────────────────────────────────
231
232
233 class TestShardIntegration:
234 def test_empty_repo_no_commits_exits_nonzero(self, repo: pathlib.Path) -> None:
235 """No commits → resolve_commit_ref returns None → exits nonzero with 'not found' message."""
236 with patch("muse.cli.commands.shard.resolve_commit_ref", return_value=None):
237 result = runner.invoke(cli, ["coord", "shard", "--agents", "4"])
238 assert result.exit_code != 0
239 assert "not found" in result.output.lower()
240
241 def test_no_manifest_files_exits_0(self, repo: pathlib.Path) -> None:
242 """Commit found but manifest is empty → prints no-semantic-files message."""
243 commit = _make_commit_stub()
244 with (
245 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
246 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value={}),
247 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value={}),
248 ):
249 result = runner.invoke(cli, ["coord", "shard", "--agents", "4"])
250 assert result.exit_code == 0
251 assert "no semantic files found" in result.output
252
253 def test_agents_zero_exits_nonzero(self, repo: pathlib.Path) -> None:
254 """--agents 0 is invalid → clamp_int raises ValueError → non-zero exit."""
255 result = runner.invoke(cli, ["coord", "shard", "--agents", "0"])
256 assert result.exit_code != 0
257
258 def test_agents_1_mocked_snapshot_text_output(self, repo: pathlib.Path) -> None:
259 commit = _make_commit_stub("deadbeef00000000")
260 sym_map = {"src/foo.py": {"foo": {}, "bar": {}}}
261 with (
262 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
263 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value={"src/foo.py": "oid1"}),
264 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
265 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
266 ):
267 result = runner.invoke(cli, ["coord", "shard", "--agents", "1"])
268 assert result.exit_code == 0
269 assert "Shard plan" in result.output
270 assert "deadbeef" in result.output
271
272 def test_format_json_produces_valid_json(self, repo: pathlib.Path) -> None:
273 commit = _make_commit_stub("cafebabe00000000")
274 sym_map = {"src/a.py": {"x": {}}, "src/b.py": {"y": {}}}
275 manifest = {"src/a.py": "oid1", "src/b.py": "oid2"}
276 with (
277 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
278 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
279 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
280 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
281 ):
282 result = runner.invoke(cli, ["coord", "shard", "--agents", "2", "--json"])
283 assert result.exit_code == 0
284 data = json.loads(result.output.strip())
285 assert "schema" in data
286 assert "commit" in data
287 assert "agents" in data
288 assert "shards_created" in data
289 assert "cross_shard_edges" in data
290 assert "shards" in data
291
292 def test_json_shorthand_same_as_json_long(self, repo: pathlib.Path) -> None:
293 commit = _make_commit_stub("00112233aabbccdd")
294 sym_map = {"src/x.py": {"f": {}}}
295 manifest = {"src/x.py": "oid1"}
296 with (
297 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
298 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
299 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
300 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
301 ):
302 r1 = runner.invoke(cli, ["coord", "shard", "--agents", "2", "--json"])
303 r2 = runner.invoke(cli, ["coord", "shard", "--agents", "2", "-j"])
304 assert r1.exit_code == 0
305 assert r2.exit_code == 0
306 d1 = json.loads(r1.output.strip())
307 d2 = json.loads(r2.output.strip())
308 # duration_ms differs between runs — compare structural fields only
309 for key in ("schema", "commit", "full_commit_id", "agents",
310 "shards_created", "total_files", "total_symbols",
311 "cross_shard_edges", "shards"):
312 assert d1[key] == d2[key]
313
314 def test_language_filter_passed_through(self, repo: pathlib.Path) -> None:
315 commit = _make_commit_stub()
316 with (
317 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
318 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value={}),
319 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value={}) as mock_sym,
320 ):
321 runner.invoke(cli, ["coord", "shard", "--agents", "2", "--language", "Python"])
322 mock_sym.assert_called_once()
323 _, kwargs = mock_sym.call_args
324 assert kwargs.get("language_filter") == "Python"
325
326 def test_commit_ref_forwarded_to_resolver(self, repo: pathlib.Path) -> None:
327 with patch("muse.cli.commands.shard.resolve_commit_ref", return_value=None) as mock_res:
328 runner.invoke(cli, ["coord", "shard", "--commit", "HEAD~3"])
329 mock_res.assert_called_once()
330 args, _ = mock_res.call_args
331 assert args[3] == "HEAD~3"
332
333 def test_text_output_contains_cross_shard_edges(self, repo: pathlib.Path) -> None:
334 commit = _make_commit_stub("aabbccdd11223344")
335 sym_map = {"src/a.py": {"f": {}}, "src/b.py": {"g": {}}}
336 manifest = {"src/a.py": "oid1", "src/b.py": "oid2"}
337 with (
338 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
339 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
340 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
341 patch("muse.cli.commands.shard._build_import_edges", return_value=[("src/a.py", "src/b.py")]),
342 ):
343 result = runner.invoke(cli, ["coord", "shard", "--agents", "2"])
344 assert result.exit_code == 0
345 assert "Cross-shard edges" in result.output
346
347
348 # ── Security ──────────────────────────────────────────────────────────────────
349
350
351 class TestShardSecurity:
352 def test_language_filter_does_not_open_filesystem(self, repo: pathlib.Path, tmp_path: pathlib.Path) -> None:
353 """--language must not cause FS traversal beyond object store."""
354 commit = _make_commit_stub()
355 with (
356 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
357 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value={}),
358 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value={}),
359 ):
360 result = runner.invoke(cli, ["coord", "shard", "--language", "../../../etc/passwd"])
361 assert result.exit_code == 0
362 assert "no semantic files found" in result.output
363
364 def test_traversal_commit_ref_handled_gracefully(self, repo: pathlib.Path) -> None:
365 """Malicious --commit ref should not crash the process."""
366 with patch("muse.cli.commands.shard.resolve_commit_ref", return_value=None):
367 result = runner.invoke(cli, ["coord", "shard", "--commit", "../../etc/shadow"])
368 assert result.exit_code == 0 or result.exit_code != 0 # no crash
369
370 def test_ansi_in_language_stripped_text_output(self, repo: pathlib.Path) -> None:
371 """ANSI escape in --language value must not appear in text output."""
372 evil_lang = "\x1b[31mPython\x1b[0m"
373 commit = _make_commit_stub()
374 with (
375 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
376 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value={}),
377 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value={}),
378 ):
379 result = runner.invoke(cli, ["coord", "shard", "--language", evil_lang])
380 assert "\x1b[" not in result.output
381
382 def test_ansi_in_file_path_stripped_text_output(self, repo: pathlib.Path) -> None:
383 """ANSI escape codes in file paths must be stripped before display."""
384 commit = _make_commit_stub("deadbeef00000000")
385 evil_fp = "\x1b[31msrc/evil.py\x1b[0m"
386 sym_map = {evil_fp: {"fn": {}}}
387 manifest = {evil_fp: "oid1"}
388 with (
389 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
390 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
391 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
392 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
393 ):
394 result = runner.invoke(cli, ["coord", "shard", "--agents", "1"])
395 assert "\x1b[" not in result.output
396 assert "src/evil.py" in result.output
397
398
399 # ── Input validation ──────────────────────────────────────────────────────────
400
401
402 class TestShardInputValidation:
403 def test_agents_zero_exits_1_clean(self, repo: pathlib.Path) -> None:
404 """--agents 0 must exit 1 with a clean error message, no traceback."""
405 result = runner.invoke(cli, ["coord", "shard", "--agents", "0"])
406 assert result.exit_code == 1
407 assert "Traceback" not in result.output
408
409 def test_agents_negative_exits_1(self, repo: pathlib.Path) -> None:
410 result = runner.invoke(cli, ["coord", "shard", "--agents", "-1"])
411 assert result.exit_code == 1
412
413 def test_agents_over_max_exits_1(self, repo: pathlib.Path) -> None:
414 result = runner.invoke(cli, ["coord", "shard", "--agents", str(_MAX_AGENTS + 1)])
415 assert result.exit_code == 1
416
417 def test_agents_at_min_accepted(self, repo: pathlib.Path) -> None:
418 commit = _make_commit_stub()
419 with (
420 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
421 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value={}),
422 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value={}),
423 ):
424 result = runner.invoke(cli, ["coord", "shard", "--agents", str(_MIN_AGENTS)])
425 assert result.exit_code == 0
426
427 def test_agents_at_max_accepted(self, repo: pathlib.Path) -> None:
428 commit = _make_commit_stub()
429 with (
430 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
431 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value={}),
432 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value={}),
433 ):
434 result = runner.invoke(cli, ["coord", "shard", "--agents", str(_MAX_AGENTS)])
435 assert result.exit_code == 0
436
437 def test_agents_invalid_json_error_shape(self, repo: pathlib.Path) -> None:
438 """--format json error for --agents out of range must have {error, status}."""
439 result = runner.invoke(cli, ["coord", "shard", "--agents", "0", "--json"])
440 assert result.exit_code == 1
441 data = json.loads(result.output.strip())
442 assert "error" in data
443 assert data["status"] == "bad_args"
444
445 def test_agents_invalid_text_uses_tick_prefix(self, repo: pathlib.Path) -> None:
446 result = runner.invoke(cli, ["coord", "shard", "--agents", "0"])
447 assert result.exit_code == 1
448 assert "❌" in result.output
449
450 def test_agents_invalid_text_no_stdout(self, repo: pathlib.Path) -> None:
451 """Text mode error goes to stderr; stdout must be empty."""
452 result = runner.invoke(cli, ["coord", "shard", "--agents", "0"])
453 # CliRunner merges stderr into output — we just check no traceback
454 assert "Traceback" not in result.output
455
456 def test_commit_not_found_json_error_shape(self, repo: pathlib.Path) -> None:
457 with patch("muse.cli.commands.shard.resolve_commit_ref", return_value=None):
458 result = runner.invoke(cli, ["coord", "shard", "--json"])
459 assert result.exit_code == 1
460 data = json.loads(result.output.strip())
461 assert "error" in data
462 assert data["status"] == "commit_not_found"
463
464 def test_commit_not_found_text_uses_tick_prefix(self, repo: pathlib.Path) -> None:
465 with patch("muse.cli.commands.shard.resolve_commit_ref", return_value=None):
466 result = runner.invoke(cli, ["coord", "shard"])
467 assert result.exit_code == 1
468 assert "❌" in result.output
469
470 def test_agents_validation_fires_before_repo_lookup(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
471 """Invalid --agents exits before trying to open .muse/ (no repo needed)."""
472 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) # no .muse dir
473 result = runner.invoke(cli, ["coord", "shard", "--agents", "0"])
474 assert result.exit_code == 1
475 # Must not say "Repository not found"
476 assert "repository" not in result.output.lower()
477
478
479 # ── JSON schema: new fields ───────────────────────────────────────────────────
480
481
482 class TestShardJsonSchema:
483 def _base_invoke(self, repo: pathlib.Path, agents: str = "2", extra: list[str] | None = None) -> tuple[InvokeResult, MagicMock]:
484 commit = _make_commit_stub("abcdef1234567890abcdef1234567890")
485 sym_map = {"src/a.py": {"f": {}}, "src/b.py": {"g": {}}}
486 manifest = {"src/a.py": "oid1", "src/b.py": "oid2"}
487 args = ["coord", "shard", "--agents", agents, "--json"]
488 if extra:
489 args.extend(extra)
490 with (
491 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
492 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
493 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
494 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
495 ):
496 return runner.invoke(cli, args), commit
497
498 def test_full_commit_id_present_and_full(self, repo: pathlib.Path) -> None:
499 result, commit = self._base_invoke(repo)
500 assert result.exit_code == 0
501 data = json.loads(result.output.strip())
502 assert "full_commit_id" in data
503 assert data["full_commit_id"] == commit.commit_id
504 assert len(data["full_commit_id"]) > 8
505
506 def test_commit_short_is_8_chars(self, repo: pathlib.Path) -> None:
507 result, commit = self._base_invoke(repo)
508 data = json.loads(result.output.strip())
509 assert data["commit"] == short_id(commit.commit_id, strip=True)
510
511 def test_total_files_correct(self, repo: pathlib.Path) -> None:
512 result, _ = self._base_invoke(repo)
513 data = json.loads(result.output.strip())
514 assert data["total_files"] == 2
515
516 def test_total_symbols_correct(self, repo: pathlib.Path) -> None:
517 result, _ = self._base_invoke(repo)
518 data = json.loads(result.output.strip())
519 # sym_map has 1 symbol per file × 2 files
520 assert data["total_symbols"] == 2
521
522 def test_duration_ms_present_and_non_negative(self, repo: pathlib.Path) -> None:
523 result, _ = self._base_invoke(repo)
524 data = json.loads(result.output.strip())
525 assert "duration_ms" in data
526 assert isinstance(data["duration_ms"], float)
527 assert data["duration_ms"] >= 0
528
529 def test_json_is_single_line(self, repo: pathlib.Path) -> None:
530 result, _ = self._base_invoke(repo)
531 lines = [ln for ln in result.output.splitlines() if ln.strip()]
532 assert len(lines) == 1, f"JSON output must be one line, got {len(lines)}"
533
534 def test_all_schema_fields_present(self, repo: pathlib.Path) -> None:
535 result, _ = self._base_invoke(repo)
536 data = json.loads(result.output.strip())
537 required = {
538 "schema", "commit", "full_commit_id", "agents",
539 "shards_created", "total_files", "total_symbols",
540 "cross_shard_edges", "shards", "duration_ms",
541 }
542 missing = required - data.keys()
543 assert not missing, f"Missing JSON fields: {missing}"
544
545 def test_no_files_case_emits_valid_schema(self, repo: pathlib.Path) -> None:
546 """Empty manifest → shards=[], total_files=0, still valid JSON schema."""
547 commit = _make_commit_stub("abcdef1234567890abcdef1234567890")
548 with (
549 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
550 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value={}),
551 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value={}),
552 ):
553 result = runner.invoke(cli, ["coord", "shard", "--agents", "4", "--json"])
554 assert result.exit_code == 0
555 data = json.loads(result.output.strip())
556 assert data["shards"] == []
557 assert data["total_files"] == 0
558 assert data["total_symbols"] == 0
559 assert "duration_ms" in data
560
561 def test_cross_shard_edges_zero_when_isolated(self, repo: pathlib.Path) -> None:
562 commit = _make_commit_stub()
563 sym_map = {"src/a.py": {"f": {}}, "src/b.py": {"g": {}}}
564 manifest = {"src/a.py": "oid1", "src/b.py": "oid2"}
565 with (
566 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
567 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
568 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
569 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
570 ):
571 result = runner.invoke(cli, ["coord", "shard", "--agents", "2", "--json"])
572 data = json.loads(result.output.strip())
573 assert data["cross_shard_edges"] == 0
574
575 def test_shards_created_capped_at_components(self, repo: pathlib.Path) -> None:
576 """shards_created = min(agents, components) — can't exceed file count."""
577 commit = _make_commit_stub()
578 sym_map = {"src/a.py": {"f": {}}} # 1 file → 1 component
579 manifest = {"src/a.py": "oid1"}
580 with (
581 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
582 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
583 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
584 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
585 ):
586 result = runner.invoke(cli, ["coord", "shard", "--agents", "10", "--json"])
587 data = json.loads(result.output.strip())
588 assert data["shards_created"] == 1
589
590
591 # ── Unit: _connected_components edge cases ────────────────────────────────────
592
593
594 class TestConnectedComponentsExtra:
595 def test_cycle_resolved_as_single_component(self) -> None:
596 """A → B → C → A cycle must yield one component of 3 files."""
597 from muse.cli.commands.shard import _connected_components
598 files = ["a.py", "b.py", "c.py"]
599 edges = [("a.py", "b.py"), ("b.py", "c.py"), ("c.py", "a.py")]
600 result = _connected_components(files, edges)
601 assert len(result) == 1
602 assert result[0] == frozenset({"a.py", "b.py", "c.py"})
603
604 def test_star_topology(self) -> None:
605 """Hub → 4 spokes: all connected, 1 component."""
606 from muse.cli.commands.shard import _connected_components
607 files = ["hub.py", "s1.py", "s2.py", "s3.py", "s4.py"]
608 edges = [(f"s{i}.py", "hub.py") for i in range(1, 5)]
609 result = _connected_components(files, edges)
610 assert len(result) == 1
611
612 def test_self_loop_ignored(self) -> None:
613 """A file importing itself produces no cross-edge (target == file_path guard)."""
614 from muse.cli.commands.shard import _connected_components
615 result = _connected_components(["a.py"], [("a.py", "a.py")])
616 assert len(result) == 1
617
618 def test_extra_edge_node_not_in_files_ignored(self) -> None:
619 """An edge referencing a file not in the files list should not crash."""
620 from muse.cli.commands.shard import _connected_components
621 # "ghost.py" is in the edge but not in files — adj.setdefault handles it
622 result = _connected_components(["a.py"], [("a.py", "ghost.py")])
623 # a.py is still returned as its own component
624 assert any("a.py" in c for c in result)
625
626
627 # ── Unit: _greedy_partition extra ────────────────────────────────────────────
628
629
630 class TestGreedyPartitionExtra:
631 def test_all_files_accounted_for(self) -> None:
632 """Every file in input components appears in exactly one shard."""
633 from muse.cli.commands.shard import _greedy_partition
634 N = 20
635 comps = [frozenset({f"f{i}.py"}) for i in range(N)]
636 sym_counts = {f"f{i}.py": i + 1 for i in range(N)}
637 shards = _greedy_partition(comps, sym_counts, n_shards=4)
638 all_files = set()
639 for s in shards:
640 assert not (all_files & s), "File appears in more than one shard"
641 all_files |= s
642 expected = {f"f{i}.py" for i in range(N)}
643 assert all_files == expected
644
645 def test_symbol_count_weighting(self) -> None:
646 """Largest component goes to the first shard (LPT first step)."""
647 from muse.cli.commands.shard import _greedy_partition
648 big = frozenset({"big.py"})
649 smalls = [frozenset({f"s{i}.py"}) for i in range(3)]
650 sym_counts = {"big.py": 100, "s0.py": 1, "s1.py": 1, "s2.py": 1}
651 shards = _greedy_partition([big] + smalls, sym_counts, n_shards=2)
652 # big.py is in one shard by itself (100 >> 3*1)
653 big_shard = next(s for s in shards if "big.py" in s)
654 assert big_shard == frozenset({"big.py"})
655
656 def test_single_large_component_into_many_shards(self) -> None:
657 """One big component split into 4 shards — all files in first shard."""
658 from muse.cli.commands.shard import _greedy_partition
659 comp = frozenset({"a.py", "b.py", "c.py"})
660 sym_counts = {"a.py": 10, "b.py": 5, "c.py": 3}
661 shards = _greedy_partition([comp], sym_counts, n_shards=4)
662 non_empty = [s for s in shards if s]
663 assert len(non_empty) == 1
664 assert non_empty[0] == comp
665
666
667 # ── Stress tests ──────────────────────────────────────────────────────────────
668
669
670 class TestShardStressExtra:
671 def test_500_isolated_files_16_shards_under_2s(self, repo: pathlib.Path) -> None:
672 """500 isolated files partitioned into 16 shards in < 2 s."""
673 N = 500
674 commit = _make_commit_stub()
675 sym_map = {f"src/mod{i}.py": {f"fn_{i}": {}} for i in range(N)}
676 manifest = {fp: f"oid{i}" for i, fp in enumerate(sym_map)}
677 with (
678 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
679 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
680 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
681 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
682 ):
683 t0 = time.monotonic()
684 result = runner.invoke(cli, ["coord", "shard", "--agents", "16", "--json"])
685 elapsed = time.monotonic() - t0
686 assert result.exit_code == 0
687 assert elapsed < 2.0, f"500 isolated files took {elapsed:.2f}s"
688 data = json.loads(result.output.strip())
689 assert data["total_files"] == N
690 assert data["total_symbols"] == N # 1 sym per file
691
692 def test_200_chain_files_4_shards_cross_edges_correct(self, repo: pathlib.Path) -> None:
693 """Chain graph: mod0→mod1→…→mod199, 4 shards — cross_shard_edges is exact."""
694 N = 200
695 commit = _make_commit_stub()
696 sym_map = {f"src/mod{i}.py": {f"fn_{i}": {}} for i in range(N)}
697 manifest = {fp: f"oid{i}" for i, fp in enumerate(sym_map)}
698 # A chain: each file imports the next
699 edges = [(f"src/mod{i}.py", f"src/mod{i+1}.py") for i in range(N - 1)]
700 with (
701 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
702 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
703 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
704 patch("muse.cli.commands.shard._build_import_edges", return_value=edges),
705 ):
706 result = runner.invoke(cli, ["coord", "shard", "--agents", "4", "--json"])
707 assert result.exit_code == 0
708 data = json.loads(result.output.strip())
709 # Chain is one big component → 1 shard, no cross-shard edges
710 assert data["shards_created"] == 1
711 assert data["cross_shard_edges"] == 0
712
713 def test_json_compact_with_500_shards(self, repo: pathlib.Path) -> None:
714 """Even with many shards, JSON output is a single compact line."""
715 N = 500
716 commit = _make_commit_stub()
717 sym_map = {f"src/mod{i}.py": {f"fn_{i}": {}} for i in range(N)}
718 manifest = {fp: f"oid{i}" for i, fp in enumerate(sym_map)}
719 with (
720 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
721 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
722 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
723 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
724 ):
725 result = runner.invoke(cli, ["coord", "shard", "--agents", "256", "--json"])
726 assert result.exit_code == 0
727 lines = [ln for ln in result.output.splitlines() if ln.strip()]
728 assert len(lines) == 1
729
730
731 # ── E2E tests ─────────────────────────────────────────────────────────────────
732
733
734 class TestShardE2E:
735 def test_single_file_one_shard_zero_edges(self, repo: pathlib.Path) -> None:
736 commit = _make_commit_stub()
737 sym_map = {"src/only.py": {"fn": {}}}
738 manifest = {"src/only.py": "oid1"}
739 with (
740 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
741 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
742 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
743 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
744 ):
745 result = runner.invoke(cli, ["coord", "shard", "--agents", "4", "--json"])
746 assert result.exit_code == 0
747 data = json.loads(result.output.strip())
748 assert data["shards_created"] == 1
749 assert data["cross_shard_edges"] == 0
750 assert data["total_files"] == 1
751
752 def test_two_disconnected_clusters_zero_cross_edges(self, repo: pathlib.Path) -> None:
753 """Two disconnected clusters into 2 shards → 0 cross-shard edges."""
754 commit = _make_commit_stub()
755 sym_map = {
756 "src/a.py": {"fa": {}}, "src/b.py": {"fb": {}}, # cluster 1
757 "src/c.py": {"fc": {}}, "src/d.py": {"fd": {}}, # cluster 2
758 }
759 manifest = {k: f"oid{i}" for i, k in enumerate(sym_map)}
760 edges = [("src/a.py", "src/b.py"), ("src/c.py", "src/d.py")]
761 with (
762 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
763 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
764 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
765 patch("muse.cli.commands.shard._build_import_edges", return_value=edges),
766 ):
767 result = runner.invoke(cli, ["coord", "shard", "--agents", "2", "--json"])
768 assert result.exit_code == 0
769 data = json.loads(result.output.strip())
770 assert data["shards_created"] == 2
771 assert data["cross_shard_edges"] == 0
772
773 def test_connected_pair_forced_into_two_shards_has_edges(self, repo: pathlib.Path) -> None:
774 """A→B with agents=2 forces a cross-shard edge (both in same component)."""
775 commit = _make_commit_stub()
776 sym_map = {
777 "src/a.py": {"fa": {}}, "src/b.py": {"fb": {}},
778 "src/c.py": {"fc": {}}, # third file so components>1 is possible
779 }
780 manifest = {k: f"oid{i}" for i, k in enumerate(sym_map)}
781 # a→b are connected (1 component), c is isolated (1 component)
782 # agents=2 → 2 shards; a+b are same component → same shard → 0 cross edges
783 edges = [("src/a.py", "src/b.py")]
784 with (
785 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
786 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
787 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
788 patch("muse.cli.commands.shard._build_import_edges", return_value=edges),
789 ):
790 result = runner.invoke(cli, ["coord", "shard", "--agents", "2", "--json"])
791 assert result.exit_code == 0
792 data = json.loads(result.output.strip())
793 # a+b are in the same component, never split → 0 cross-shard edges
794 assert data["cross_shard_edges"] == 0
795
796 def test_text_output_shows_elapsed(self, repo: pathlib.Path) -> None:
797 commit = _make_commit_stub("cafebabe00000000")
798 sym_map = {"src/x.py": {"f": {}}}
799 manifest = {"src/x.py": "oid1"}
800 with (
801 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
802 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
803 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
804 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
805 ):
806 result = runner.invoke(cli, ["coord", "shard", "--agents", "1"])
807 assert result.exit_code == 0
808 assert "s)" in result.output
809
810 def test_text_output_perfect_isolation_message(self, repo: pathlib.Path) -> None:
811 """When cross_shard_edges == 0, text output says 'Perfect isolation'."""
812 commit = _make_commit_stub()
813 sym_map = {"src/a.py": {"f": {}}}
814 manifest = {"src/a.py": "oid1"}
815 with (
816 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
817 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
818 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
819 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
820 ):
821 result = runner.invoke(cli, ["coord", "shard", "--agents", "1"])
822 assert result.exit_code == 0
823 assert "Perfect isolation" in result.output
824
825 def test_symbol_count_sum_equals_total_symbols(self, repo: pathlib.Path) -> None:
826 """Sum of shard symbol_counts must equal total_symbols in JSON."""
827 commit = _make_commit_stub()
828 sym_map = {f"src/f{i}.py": {f"fn{j}": {} for j in range(i + 1)} for i in range(5)}
829 manifest = {k: f"oid{i}" for i, k in enumerate(sym_map)}
830 with (
831 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
832 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
833 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
834 patch("muse.cli.commands.shard._build_import_edges", return_value=[]),
835 ):
836 result = runner.invoke(cli, ["coord", "shard", "--agents", "3", "--json"])
837 assert result.exit_code == 0
838 data = json.loads(result.output.strip())
839 assert sum(s["symbol_count"] for s in data["shards"]) == data["total_symbols"]
840
841
842 # ── Stress ────────────────────────────────────────────────────────────────────
843
844
845 class TestShardStress:
846 def test_100_files_8_shards_under_2s(self, repo: pathlib.Path) -> None:
847 n_files = 100
848 commit = _make_commit_stub()
849 sym_map = {f"src/mod{i}.py": {f"fn_{i}": {}, f"cls_{i}": {}} for i in range(n_files)}
850 manifest = {fp: f"oid{i}" for i, fp in enumerate(sym_map)}
851 edges = [(f"src/mod{i}.py", f"src/mod{i+1}.py") for i in range(0, n_files - 1, 5)]
852
853 with (
854 patch("muse.cli.commands.shard.resolve_commit_ref", return_value=commit),
855 patch("muse.cli.commands.shard.get_commit_snapshot_manifest", return_value=manifest),
856 patch("muse.cli.commands.shard.symbols_for_snapshot", return_value=sym_map),
857 patch("muse.cli.commands.shard._build_import_edges", return_value=edges),
858 ):
859 t0 = time.monotonic()
860 result = runner.invoke(cli, ["coord", "shard", "--agents", "8", "--json"])
861 elapsed = time.monotonic() - t0
862
863 assert result.exit_code == 0
864 assert elapsed < 2.0
865 data = json.loads(result.output.strip())
866 assert data["shards_created"] <= 8
867 assert sum(s["symbol_count"] for s in data["shards"]) == n_files * 2
868
869
870 class TestRegisterFlags:
871 def test_default_json_out_is_false(self):
872 import argparse
873 from muse.cli.commands.shard import register
874 p = argparse.ArgumentParser()
875 subs = p.add_subparsers()
876 register(subs)
877 args = p.parse_args(["shard"])
878 assert args.json_out is False
879
880 def test_json_flag_sets_json_out(self):
881 import argparse
882 from muse.cli.commands.shard import register
883 p = argparse.ArgumentParser()
884 subs = p.add_subparsers()
885 register(subs)
886 args = p.parse_args(["shard", "--json"])
887 assert args.json_out is True
888
889 def test_j_shorthand_sets_json_out(self):
890 import argparse
891 from muse.cli.commands.shard import register
892 p = argparse.ArgumentParser()
893 subs = p.add_subparsers()
894 register(subs)
895 args = p.parse_args(["shard", "-j"])
896 assert args.json_out is True
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 142 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 148 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 151 days ago