"""Directory changes in ``muse commit`` and ``muse read`` output. When a commit adds or removes empty directories, those changes must be visible in both the commit summary line and in ``muse read --json``, alongside the existing ``files_added`` / ``files_removed`` fields. Coverage matrix --------------- DC Directory changes in commit / read output DC1 commit adding an empty dir → dirs_added: ["foobar/"] in muse read DC2 commit removing a committed dir → dirs_removed: ["emptydir/"] in muse read DC3 total_changes in muse read includes dir changes DC4 commit summary line mentions directory additions ("1 dir added") DC5 commit summary line mentions directory removals ("1 dir removed") DC6 commit with only file changes → dirs_added: [] and dirs_removed: [] DC7 dirs_added and dirs_removed always present in muse read --json """ from __future__ import annotations import json import pathlib from collections.abc import Mapping import pytest from tests.cli_test_helper import CliRunner cli = None runner = CliRunner() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _env(root: pathlib.Path) -> Mapping[str, str]: return {"MUSE_REPO_ROOT": str(root)} def _run(root: pathlib.Path, *args: str) -> str: result = runner.invoke(cli, list(args), env=_env(root)) assert result.exit_code == 0, f"{args} failed:\n{result.output}\n{result.stderr}" return result.output.strip() def _read(root: pathlib.Path) -> Mapping[str, object]: return json.loads(_run(root, "read", "--json")) # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def base_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: """Code repo with one committed file, clean working tree.""" monkeypatch.chdir(tmp_path) _run(tmp_path, "init", "--domain", "code") (tmp_path / "main.py").write_text("x = 1\n") _run(tmp_path, "code", "add", ".") _run(tmp_path, "commit", "-m", "initial") return tmp_path @pytest.fixture() def repo_with_committed_dir( tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> pathlib.Path: """Code repo with a committed empty directory ``emptydir``.""" monkeypatch.chdir(tmp_path) _run(tmp_path, "init", "--domain", "code") (tmp_path / "main.py").write_text("x = 1\n") (tmp_path / "emptydir").mkdir() _run(tmp_path, "code", "add", ".") _run(tmp_path, "commit", "-m", "initial with emptydir") return tmp_path # --------------------------------------------------------------------------- # DC Directory changes in commit / read # --------------------------------------------------------------------------- class TestCommitDirChanges: def test_DC1_dirs_added_in_read_json(self, base_repo: pathlib.Path) -> None: """DC1: commit adding an empty dir → dirs_added: ['foobar/'] in muse read.""" root = base_repo (root / "foobar").mkdir() _run(root, "code", "add", "foobar") _run(root, "commit", "-m", "add foobar dir") data = _read(root) assert "dirs_added" in data, "muse read --json must have a 'dirs_added' key" assert "foobar/" in data["dirs_added"], ( "newly committed empty dir must appear in dirs_added with trailing slash" ) def test_DC2_dirs_removed_in_read_json( self, repo_with_committed_dir: pathlib.Path ) -> None: """DC2: commit removing a committed dir → dirs_removed: ['emptydir/'].""" root = repo_with_committed_dir (root / "emptydir").rmdir() _run(root, "code", "add", ".") _run(root, "commit", "-m", "remove emptydir") data = _read(root) assert "dirs_removed" in data, "muse read --json must have a 'dirs_removed' key" assert "emptydir/" in data["dirs_removed"], ( "removed committed dir must appear in dirs_removed with trailing slash" ) def test_DC3_total_changes_includes_dirs(self, base_repo: pathlib.Path) -> None: """DC3: total_changes in muse read includes directory additions.""" root = base_repo (root / "foobar").mkdir() _run(root, "code", "add", "foobar") _run(root, "commit", "-m", "add foobar dir") data = _read(root) assert data["total_changes"] == 1, ( "adding one empty dir must count as 1 total change" ) def test_DC4_commit_summary_mentions_dir_added( self, base_repo: pathlib.Path ) -> None: """DC4: commit summary line mentions directory additions.""" root = base_repo (root / "foobar").mkdir() _run(root, "code", "add", "foobar") result = runner.invoke( cli, ["commit", "-m", "add foobar dir"], env=_env(root) ) assert result.exit_code == 0 assert "dir" in result.output.lower(), ( f"commit output must mention directory change; got:\n{result.output}" ) def test_DC5_commit_summary_mentions_dir_removed( self, repo_with_committed_dir: pathlib.Path ) -> None: """DC5: commit summary line mentions directory removals.""" root = repo_with_committed_dir (root / "emptydir").rmdir() _run(root, "code", "add", ".") result = runner.invoke( cli, ["commit", "-m", "remove emptydir"], env=_env(root) ) assert result.exit_code == 0 assert "dir" in result.output.lower(), ( f"commit output must mention directory removal; got:\n{result.output}" ) def test_DC6_no_dir_changes_gives_empty_lists( self, base_repo: pathlib.Path ) -> None: """DC6: commit with only file changes → dirs_added: [] and dirs_removed: [].""" root = base_repo (root / "other.py").write_text("y = 2\n") _run(root, "code", "add", ".") _run(root, "commit", "-m", "add other.py") data = _read(root) assert data["dirs_added"] == [] assert data["dirs_removed"] == [] def test_DC7_dirs_keys_always_present(self, base_repo: pathlib.Path) -> None: """DC7: dirs_added and dirs_removed always present in muse read --json.""" data = _read(base_repo) assert "dirs_added" in data assert "dirs_removed" in data assert isinstance(data["dirs_added"], list) assert isinstance(data["dirs_removed"], list)