"""Tests for muse/core/workspace.py — multi-repository workspace management.""" from __future__ import annotations import inspect import json import pathlib import time import pytest from muse.core.types import NULL_COMMIT_ID from muse.core.workspace import ( WorkspaceMemberStatus, add_workspace_member, list_workspace_members, remove_workspace_member, ) from muse.core.paths import muse_dir, workspace_toml_path # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: muse = muse_dir(tmp_path) for d in ("objects", "commits", "snapshots", "refs/heads"): (muse / d).mkdir(parents=True, exist_ok=True) (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"})) (muse / "HEAD").write_text("ref: refs/heads/main\n") (muse / "refs" / "heads" / "main").write_text(NULL_COMMIT_ID) return tmp_path # --------------------------------------------------------------------------- # add_workspace_member # --------------------------------------------------------------------------- def test_add_member_creates_manifest(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "core", "https://musehub.ai/acme/core") manifest_path = workspace_toml_path(repo) assert manifest_path.exists() def test_add_member_stores_name_and_url(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "sounds", "https://musehub.ai/acme/sounds") members = list_workspace_members(repo) assert len(members) == 1 assert members[0].name == "sounds" assert members[0].url == "https://musehub.ai/acme/sounds" def test_add_member_default_path(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "core", "https://example.com/core") members = list_workspace_members(repo) assert "repos/core" in str(members[0].path) def test_add_member_custom_path(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "core", "https://example.com/core", path="vendor/core") members = list_workspace_members(repo) assert "vendor/core" in str(members[0].path) def test_add_member_default_branch_is_main(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "core", "https://example.com/core") members = list_workspace_members(repo) assert members[0].branch == "main" def test_add_member_custom_branch(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "data", "https://example.com/data", branch="v2") members = list_workspace_members(repo) assert members[0].branch == "v2" def test_add_duplicate_member_raises(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "core", "https://example.com/core") with pytest.raises(ValueError, match="already exists"): add_workspace_member(repo, "core", "https://example.com/other") def test_add_multiple_members(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "core", "https://example.com/core") add_workspace_member(repo, "sounds", "https://example.com/sounds") add_workspace_member(repo, "docs", "https://example.com/docs") members = list_workspace_members(repo) assert len(members) == 3 # --------------------------------------------------------------------------- # remove_workspace_member # --------------------------------------------------------------------------- def test_remove_member_removes_from_manifest(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "core", "https://example.com/core") remove_workspace_member(repo, "core") members = list_workspace_members(repo) assert len(members) == 0 def test_remove_nonexistent_member_raises(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) # First add a member so the manifest exists, then try to remove a nonexistent one. add_workspace_member(repo, "core", "https://example.com/core") with pytest.raises(ValueError, match="not found"): remove_workspace_member(repo, "nonexistent") def test_remove_no_manifest_raises(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) with pytest.raises(ValueError, match="No workspace manifest"): remove_workspace_member(repo, "anything") def test_remove_only_removes_named_member(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "core", "https://example.com/core") add_workspace_member(repo, "sounds", "https://example.com/sounds") remove_workspace_member(repo, "core") members = list_workspace_members(repo) assert len(members) == 1 assert members[0].name == "sounds" # --------------------------------------------------------------------------- # list_workspace_members # --------------------------------------------------------------------------- def test_list_returns_empty_when_no_manifest(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) assert list_workspace_members(repo) == [] def test_list_present_false_when_not_cloned(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "core", "https://example.com/core") members = list_workspace_members(repo) assert members[0].present is False def test_list_present_true_when_cloned(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "local", str(tmp_path / "local_clone"), path="local_clone") # Simulate a cloned repo at the expected path. clone_path = tmp_path / "local_clone" muse_dir(clone_path).mkdir(parents=True, exist_ok=True) members = list_workspace_members(repo) assert members[0].present is True def test_list_head_none_when_not_cloned(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "core", "https://example.com/core") members = list_workspace_members(repo) assert members[0].head_commit is None def test_list_returns_workspace_member_status(tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) add_workspace_member(repo, "core", "https://example.com/core") members = list_workspace_members(repo) assert isinstance(members[0], WorkspaceMemberStatus) # --------------------------------------------------------------------------- # Performance — parallelism and no-subprocess shelf count # --------------------------------------------------------------------------- def _make_present_repo(parent: pathlib.Path, name: str) -> pathlib.Path: """Create a minimal present repo inside *parent* and return its path.""" repo = parent / name muse = muse_dir(repo) for d in ("objects", "commits", "snapshots", "refs/heads"): (muse / d).mkdir(parents=True, exist_ok=True) (muse / "repo.json").write_text(json.dumps({"repo_id": f"test-{name}"})) (muse / "HEAD").write_text("ref: refs/heads/main\n") (muse / "refs" / "heads" / "main").write_text(NULL_COMMIT_ID) (muse / "shelf.json").write_text("[]") return repo def test_list_workspace_members_runs_in_parallel(tmp_path: pathlib.Path) -> None: """list_workspace_members must dispatch per-member work concurrently. We verify this structurally: list_workspace_members (or the helper it calls) must use concurrent.futures.ThreadPoolExecutor — a sequential implementation would be unacceptably slow for large workspaces. """ import concurrent.futures from muse.core import workspace as ws_module source = inspect.getsource(ws_module) assert "ThreadPoolExecutor" in source, ( "list_workspace_members must use ThreadPoolExecutor for parallelism" ) def test_shelf_count_does_not_spawn_subprocess(tmp_path: pathlib.Path) -> None: """Shelf count must be read from .muse/shelf.json, not via subprocess. A subprocess for every member is the single largest performance cost. The shelf count is a simple list-length read — no subprocess needed. """ from muse.core import workspace as ws_module source = inspect.getsource(ws_module) # No subprocess spawning for shelf count. assert '"muse", "shelf"' not in source, ( "_member_status must not spawn 'muse shelf list' — read shelf.json directly" ) def test_shelf_count_read_from_file(tmp_path: pathlib.Path) -> None: """shelf_count reflects entries in .muse/shelf.json without any subprocess.""" workspace = _make_repo(tmp_path) member_path = tmp_path / "member_a" _make_present_repo(tmp_path, "member_a") # Write two shelf entries directly to shelf.json. shelf_data = [{"id": "aaa", "intent": "wip1"}, {"id": "bbb", "intent": "wip2"}] (muse_dir(member_path) / "shelf.json").write_text(json.dumps(shelf_data)) add_workspace_member(workspace, "member_a", "https://example.com/a", path="member_a") members = list_workspace_members(workspace) assert members[0].shelf_count == 2, ( f"Expected shelf_count=2, got {members[0].shelf_count}" ) def test_shelf_count_zero_when_file_empty(tmp_path: pathlib.Path) -> None: """shelf_count is 0 when shelf.json holds an empty list.""" workspace = _make_repo(tmp_path) _make_present_repo(tmp_path, "member_b") add_workspace_member(workspace, "member_b", "https://example.com/b", path="member_b") members = list_workspace_members(workspace) assert members[0].shelf_count == 0 def test_shelf_count_zero_when_file_absent(tmp_path: pathlib.Path) -> None: """shelf_count is 0 when .muse/shelf.json does not exist.""" workspace = _make_repo(tmp_path) member_path = _make_present_repo(tmp_path, "member_c") (muse_dir(member_path) / "shelf.json").unlink() add_workspace_member(workspace, "member_c", "https://example.com/c", path="member_c") members = list_workspace_members(workspace) assert members[0].shelf_count == 0 def test_list_workspace_members_wall_time(tmp_path: pathlib.Path) -> None: """Seven present members must complete in under 2 s wall time. The sequential implementation with two subprocesses per member takes ~3–4 s. Parallelism plus file-I/O shelf counting brings this well under 1 s; 2 s is a generous upper bound that rules out regression without being fragile on slow CI machines. """ workspace = _make_repo(tmp_path) for i in range(7): _make_present_repo(tmp_path, f"repo{i}") add_workspace_member( workspace, f"repo{i}", f"https://example.com/repo{i}", path=f"repo{i}" ) t0 = time.perf_counter() members = list_workspace_members(workspace) elapsed = time.perf_counter() - t0 assert len(members) == 7 assert elapsed < 2.0, ( f"list_workspace_members took {elapsed:.2f}s for 7 members (limit 2.0s). " "Check that members are processed in parallel and shelf uses file I/O." ) # --------------------------------------------------------------------------- # Stress # --------------------------------------------------------------------------- def test_stress_50_members(tmp_path: pathlib.Path) -> None: """Adding 50 members should all be preserved and listed correctly.""" repo = _make_repo(tmp_path) for i in range(50): add_workspace_member(repo, f"svc{i}", f"https://example.com/svc{i}") members = list_workspace_members(repo) assert len(members) == 50 names = {m.name for m in members} for i in range(50): assert f"svc{i}" in names def test_stress_add_remove_cycle(tmp_path: pathlib.Path) -> None: """Add and remove 20 members; manifest should be empty at the end.""" repo = _make_repo(tmp_path) for i in range(20): add_workspace_member(repo, f"repo{i}", f"https://example.com/repo{i}") for i in range(20): remove_workspace_member(repo, f"repo{i}") assert list_workspace_members(repo) == []