"""Supercharge tests for ``muse workspace`` — agent-usability, coverage gaps. Coverage matrix --------------- - duration_ms: every JSON-outputting subcommand includes it - exit_code: every JSON-outputting subcommand includes it - branch_mismatch: boolean flag in member JSON when actual_branch != branch - list/status envelopes: {members, exit_code, duration_ms} — not bare arrays - sync exit_code: reflects error_count > 0 - TypedDicts: verify fields exist in class annotations - Docstrings: sync docstring covers JSON envelope fields - Performance: duration_ms reported within reasonable bounds """ from __future__ import annotations import argparse import json import pathlib import subprocess import sys import pytest from muse.cli.commands.workspace import ( _WorkspaceAddJson, _WorkspaceMemberJson, _WorkspaceRemoveJson, _WorkspaceSyncJson, _WorkspaceUpdateJson, _member_to_json, run_workspace_sync, ) from muse.core.types import NULL_COMMIT_ID from muse.core.paths import muse_dir from muse.core.workspace import ( WorkspaceMemberStatus, add_workspace_member, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: dot_muse = muse_dir(tmp_path) for d in ("objects", "commits", "snapshots", "refs/heads"): (dot_muse / d).mkdir(parents=True, exist_ok=True) (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"})) (dot_muse / "HEAD").write_text("ref: refs/heads/main\n") (dot_muse / "refs" / "heads" / "main").write_text(NULL_COMMIT_ID) return tmp_path def _cli(args: list[str], cwd: pathlib.Path) -> tuple[str, str, int]: result = subprocess.run( [sys.executable, "-m", "muse.cli.app"] + args, capture_output=True, text=True, cwd=str(cwd), ) return result.stdout, result.stderr, result.returncode def _add(repo: pathlib.Path, name: str = "core", url: str = "https://musehub.ai/acme/core") -> None: add_workspace_member(repo, name, url) def _fake_member( *, present: bool = False, actual_branch: str | None = None, branch: str = "main", ) -> WorkspaceMemberStatus: return WorkspaceMemberStatus( name="core", url="https://musehub.ai/acme/core", path=pathlib.Path("/tmp/core"), branch=branch, present=present, head_commit=None, dirty=False, actual_branch=actual_branch, shelf_count=0, feature_branches=[], ) # --------------------------------------------------------------------------- # duration_ms — every JSON subcommand must include it # --------------------------------------------------------------------------- class TestDurationMs: def test_add_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, rc = _cli(["workspace", "add", "core", "https://musehub.ai/acme/core", "--json"], repo) assert rc == 0 data = json.loads(out) assert "duration_ms" in data def test_update_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, rc = _cli(["workspace", "update", "core", "--branch", "dev", "--json"], repo) assert rc == 0 data = json.loads(out) assert "duration_ms" in data def test_remove_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, rc = _cli(["workspace", "remove", "core", "--json"], repo) assert rc == 0 data = json.loads(out) assert "duration_ms" in data def test_list_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, rc = _cli(["workspace", "list", "--json"], repo) assert rc == 0 data = json.loads(out) assert "duration_ms" in data def test_status_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, rc = _cli(["workspace", "status", "--json"], repo) assert rc == 0 data = json.loads(out) assert "duration_ms" in data def test_sync_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, rc = _cli(["workspace", "sync", "--dry-run", "--json"], repo) assert rc == 0 data = json.loads(out) assert "duration_ms" in data def test_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, _ = _cli(["workspace", "list", "--json"], repo) data = json.loads(out) assert isinstance(data["duration_ms"], (int, float)) def test_duration_ms_is_non_negative(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, _ = _cli(["workspace", "list", "--json"], repo) data = json.loads(out) assert data["duration_ms"] >= 0 # --------------------------------------------------------------------------- # exit_code — every JSON subcommand must include it # --------------------------------------------------------------------------- class TestExitCode: def test_add_json_has_exit_code(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, rc = _cli(["workspace", "add", "core", "https://musehub.ai/acme/core", "--json"], repo) assert rc == 0 data = json.loads(out) assert "exit_code" in data def test_update_json_has_exit_code(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, rc = _cli(["workspace", "update", "core", "--branch", "dev", "--json"], repo) assert rc == 0 data = json.loads(out) assert "exit_code" in data def test_remove_json_has_exit_code(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, rc = _cli(["workspace", "remove", "core", "--json"], repo) assert rc == 0 data = json.loads(out) assert "exit_code" in data def test_list_json_has_exit_code(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, rc = _cli(["workspace", "list", "--json"], repo) assert rc == 0 data = json.loads(out) assert "exit_code" in data def test_status_json_has_exit_code(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, rc = _cli(["workspace", "status", "--json"], repo) assert rc == 0 data = json.loads(out) assert "exit_code" in data def test_sync_json_has_exit_code(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, rc = _cli(["workspace", "sync", "--dry-run", "--json"], repo) assert rc == 0 data = json.loads(out) assert "exit_code" in data def test_add_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, rc = _cli(["workspace", "add", "core", "https://musehub.ai/acme/core", "--json"], repo) assert rc == 0 assert json.loads(out)["exit_code"] == 0 def test_add_exit_code_one_on_duplicate(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, rc = _cli(["workspace", "add", "core", "https://musehub.ai/acme/core", "--json"], repo) assert rc == 1 data = json.loads(out) assert data["exit_code"] == 1 def test_list_exit_code_zero_empty(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, rc = _cli(["workspace", "list", "--json"], repo) assert rc == 0 assert json.loads(out)["exit_code"] == 0 def test_exit_code_is_int(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, _ = _cli(["workspace", "list", "--json"], repo) data = json.loads(out) assert isinstance(data["exit_code"], int) def test_exit_code_mirrors_process_exit(self, tmp_path: pathlib.Path) -> None: """exit_code in JSON must equal the process exit code.""" repo = _make_repo(tmp_path) _add(repo) out, _, rc = _cli(["workspace", "add", "core", "https://x.com/y", "--json"], repo) data = json.loads(out) assert data["exit_code"] == rc # --------------------------------------------------------------------------- # List/Status envelopes — {members, exit_code, duration_ms}, not bare arrays # --------------------------------------------------------------------------- class TestListEnvelope: def test_list_json_is_dict_not_array(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, _ = _cli(["workspace", "list", "--json"], repo) data = json.loads(out) assert isinstance(data, dict), "list --json must return an envelope dict, not a bare array" def test_list_json_has_members_key(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, _ = _cli(["workspace", "list", "--json"], repo) data = json.loads(out) assert "members" in data def test_list_json_members_is_array(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, _ = _cli(["workspace", "list", "--json"], repo) data = json.loads(out) assert isinstance(data["members"], list) def test_list_json_members_count_matches_registered(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo, "core") _add(repo, "sounds", "https://musehub.ai/acme/sounds") out, _, _ = _cli(["workspace", "list", "--json"], repo) data = json.loads(out) assert len(data["members"]) == 2 def test_list_json_empty_envelope(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, rc = _cli(["workspace", "list", "--json"], repo) assert rc == 0 data = json.loads(out) assert data["members"] == [] assert data["exit_code"] == 0 def test_list_json_member_fields_intact(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, _ = _cli(["workspace", "list", "--json"], repo) member = json.loads(out)["members"][0] for key in ("name", "url", "path", "branch", "present", "head_commit", "dirty", "actual_branch", "shelf_count", "feature_branches"): assert key in member, f"member missing key: {key}" class TestStatusEnvelope: def test_status_json_is_dict_not_array(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, _ = _cli(["workspace", "status", "--json"], repo) data = json.loads(out) assert isinstance(data, dict), "status --json must return an envelope dict, not a bare array" def test_status_json_has_members_key(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, _ = _cli(["workspace", "status", "--json"], repo) data = json.loads(out) assert "members" in data def test_status_json_members_is_array(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, _ = _cli(["workspace", "status", "--json"], repo) data = json.loads(out) assert isinstance(data["members"], list) def test_status_named_json_is_dict(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, _ = _cli(["workspace", "status", "core", "--json"], repo) data = json.loads(out) assert isinstance(data, dict) assert "members" in data assert len(data["members"]) == 1 def test_status_json_empty_envelope(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, rc = _cli(["workspace", "status", "--json"], repo) assert rc == 0 data = json.loads(out) assert data["members"] == [] assert data["exit_code"] == 0 # --------------------------------------------------------------------------- # branch_mismatch — bool flag in member JSON # --------------------------------------------------------------------------- class TestBranchMismatch: def test_branch_mismatch_field_in_member_to_json(self) -> None: m = _fake_member(branch="main", actual_branch="main") result = _member_to_json(m) assert "branch_mismatch" in result def test_branch_mismatch_false_when_on_tracking_branch(self) -> None: m = _fake_member(branch="main", actual_branch="main") result = _member_to_json(m) assert result["branch_mismatch"] is False def test_branch_mismatch_true_when_on_different_branch(self) -> None: m = _fake_member(branch="main", actual_branch="dev") result = _member_to_json(m) assert result["branch_mismatch"] is True def test_branch_mismatch_false_when_not_present(self) -> None: """Not-cloned members have no actual branch — no mismatch.""" m = _fake_member(present=False, actual_branch=None, branch="main") result = _member_to_json(m) assert result["branch_mismatch"] is False def test_branch_mismatch_is_bool(self) -> None: m = _fake_member(branch="main", actual_branch="main") result = _member_to_json(m) assert isinstance(result["branch_mismatch"], bool) def test_branch_mismatch_in_list_json(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, _ = _cli(["workspace", "list", "--json"], repo) member = json.loads(out)["members"][0] assert "branch_mismatch" in member assert isinstance(member["branch_mismatch"], bool) def test_branch_mismatch_in_status_json(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, _ = _cli(["workspace", "status", "--json"], repo) member = json.loads(out)["members"][0] assert "branch_mismatch" in member # --------------------------------------------------------------------------- # sync exit_code reflects error_count # --------------------------------------------------------------------------- class TestSyncExitCode: def test_sync_dry_run_exit_code_zero(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _add(repo) out, _, rc = _cli(["workspace", "sync", "--dry-run", "--json"], repo) assert rc == 0 data = json.loads(out) assert data["exit_code"] == 0 def test_sync_empty_manifest_exit_code_zero(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, rc = _cli(["workspace", "sync", "--dry-run", "--json"], repo) assert rc == 0 data = json.loads(out) assert data["exit_code"] == 0 def test_sync_exit_code_is_int(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, _ = _cli(["workspace", "sync", "--dry-run", "--json"], repo) data = json.loads(out) assert isinstance(data["exit_code"], int) # --------------------------------------------------------------------------- # TypedDicts — verify annotations carry the new fields # --------------------------------------------------------------------------- class TestTypedDicts: def test_workspace_add_json_has_duration_ms_annotation(self) -> None: assert "duration_ms" in _WorkspaceAddJson.__annotations__ def test_workspace_add_json_has_exit_code_annotation(self) -> None: assert "exit_code" in _WorkspaceAddJson.__annotations__ def test_workspace_update_json_has_duration_ms_annotation(self) -> None: assert "duration_ms" in _WorkspaceUpdateJson.__annotations__ def test_workspace_update_json_has_exit_code_annotation(self) -> None: assert "exit_code" in _WorkspaceUpdateJson.__annotations__ def test_workspace_remove_json_has_duration_ms_annotation(self) -> None: assert "duration_ms" in _WorkspaceRemoveJson.__annotations__ def test_workspace_remove_json_has_exit_code_annotation(self) -> None: assert "exit_code" in _WorkspaceRemoveJson.__annotations__ def test_workspace_sync_json_has_duration_ms_annotation(self) -> None: assert "duration_ms" in _WorkspaceSyncJson.__annotations__ def test_workspace_sync_json_has_exit_code_annotation(self) -> None: assert "exit_code" in _WorkspaceSyncJson.__annotations__ def test_workspace_member_json_has_branch_mismatch_annotation(self) -> None: assert "branch_mismatch" in _WorkspaceMemberJson.__annotations__ # --------------------------------------------------------------------------- # Docstrings # --------------------------------------------------------------------------- class TestDocstrings: def test_sync_docstring_mentions_workers(self) -> None: assert "workers" in (run_workspace_sync.__doc__ or "").lower() def test_sync_docstring_mentions_dry_run(self) -> None: assert "dry" in (run_workspace_sync.__doc__ or "").lower() def test_sync_docstring_mentions_json_fields(self) -> None: doc = run_workspace_sync.__doc__ or "" assert "exit_code" in doc or "duration_ms" in doc or "json" in doc.lower() def test_sync_docstring_mentions_exit_code(self) -> None: assert "exit_code" in (run_workspace_sync.__doc__ or "") def test_sync_docstring_mentions_duration_ms(self) -> None: assert "duration_ms" in (run_workspace_sync.__doc__ or "") # --------------------------------------------------------------------------- # Performance — duration_ms stays within reason # --------------------------------------------------------------------------- class TestPerformance: def test_list_10_members_duration_ms_under_2000(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) for i in range(10): _add(repo, f"member{i}", f"https://musehub.ai/acme/m{i}") out, _, rc = _cli(["workspace", "list", "--json"], repo) assert rc == 0 data = json.loads(out) assert data["duration_ms"] < 2000, f"list of 10 took {data['duration_ms']}ms" def test_status_10_members_duration_ms_under_2000(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) for i in range(10): _add(repo, f"member{i}", f"https://musehub.ai/acme/m{i}") out, _, rc = _cli(["workspace", "status", "--json"], repo) assert rc == 0 data = json.loads(out) assert data["duration_ms"] < 2000 def test_add_duration_ms_under_500(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) out, _, rc = _cli(["workspace", "add", "core", "https://musehub.ai/acme/core", "--json"], repo) assert rc == 0 data = json.loads(out) assert data["duration_ms"] < 500 # --------------------------------------------------------------------------- # Flag registration # --------------------------------------------------------------------------- class TestRegisterFlags: def _parse(self, *args: str) -> "argparse.Namespace": import argparse from muse.cli.commands.workspace import register p = argparse.ArgumentParser() sub = p.add_subparsers() register(sub) return p.parse_args(["workspace", *args]) def test_default_json_out_is_false_add(self) -> None: ns = self._parse("add", "myrepo", "https://musehub.ai/x/y") assert ns.json_out is False def test_json_flag_sets_json_out_add(self) -> None: ns = self._parse("add", "myrepo", "https://musehub.ai/x/y", "--json") assert ns.json_out is True def test_j_shorthand_sets_json_out_add(self) -> None: ns = self._parse("add", "myrepo", "https://musehub.ai/x/y", "-j") assert ns.json_out is True