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