"""Directory tracking in ``muse status``. Directories are first-class symbols in Muse, fully symmetric with files. The only distinguisher is a trailing ``/`` on their path. Symmetry rules -------------- Untracked file → untracked: ["foo.py"] Untracked dir → untracked: ["foobar/"] Staged file → staged.added: ["foo.py"], added: ["foo.py"] Staged dir → staged.added: ["foobar/"], added: ["foobar/"] Deleted file → deleted: ["old.py"], unstaged.deleted: ["old.py"] Deleted dir → deleted: ["old/"], unstaged.deleted: ["old/"] Coverage matrix --------------- D Directory tracking in muse status D1 mkdir foobar → "foobar/" in untracked (same as an untracked file) D2 dirty=True and total_changes==0, untracked_count==1 after mkdir alone D3 after muse code add foobar → "foobar/" in staged.added and added D4 staged dir not in untracked D5 committed dir deleted → "emptydir/" in deleted and unstaged.deleted D6 non-empty dir (has files) not in untracked — covered by its files D7 file inside new dir appears in untracked, dir itself does not D8 clean repo has no trailing-slash entries anywhere """ 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: {result.output}" return result.output.strip() def _status(root: pathlib.Path) -> Mapping[str, object]: return json.loads(_run(root, "status", "--json")) @pytest.fixture() def code_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: """Code repo with one committed file and no pending changes.""" 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 # --------------------------------------------------------------------------- # D Directory tracking # --------------------------------------------------------------------------- class TestDirectoryTracking: def test_D1_new_empty_dir_in_untracked( self, code_repo: pathlib.Path ) -> None: """D1: mkdir foobar → 'foobar/' in untracked, same as an untracked file.""" root = code_repo (root / "foobar").mkdir() data = _status(root) assert "foobar/" in data["untracked"], ( "new empty dir must appear in untracked with a trailing slash" ) def test_D2_untracked_dir_counts_as_untracked_not_change( self, code_repo: pathlib.Path ) -> None: """D2: mkdir alone → dirty, untracked_count==1, total_changes==0.""" root = code_repo (root / "foobar").mkdir() data = _status(root) assert data["dirty"] is True assert data["clean"] is False assert data["untracked_count"] == 1 assert data["total_changes"] == 0 def test_D3_staged_dir_in_staged_added_and_added( self, code_repo: pathlib.Path ) -> None: """D3: after muse code add foobar → 'foobar/' in staged.added and added.""" root = code_repo (root / "foobar").mkdir() _run(root, "code", "add", "foobar") data = _status(root) assert "foobar/" in data["staged"]["added"], ( "staged dir must appear in staged.added with trailing slash" ) assert "foobar/" in data["added"], ( "staged dir must appear in flat added" ) def test_D4_staged_dir_not_in_untracked( self, code_repo: pathlib.Path ) -> None: """D4: staged directory does not appear in untracked.""" root = code_repo (root / "foobar").mkdir() _run(root, "code", "add", "foobar") data = _status(root) assert "foobar/" not in data["untracked"] def test_D5_deleted_committed_dir_in_deleted_and_unstaged( self, repo_with_committed_dir: pathlib.Path ) -> None: """D5: committed empty dir deleted → 'emptydir/' in deleted and unstaged.deleted.""" root = repo_with_committed_dir (root / "emptydir").rmdir() data = _status(root) assert "emptydir/" in data["deleted"], ( "deleted committed dir must appear in deleted with trailing slash" ) assert "emptydir/" in data["unstaged"]["deleted"], ( "deleted committed dir must appear in unstaged.deleted with trailing slash" ) def test_D6_non_empty_dir_not_in_untracked( self, code_repo: pathlib.Path ) -> None: """D6: dir with a new file — 'src/' not in untracked, its file is.""" root = code_repo (root / "src").mkdir() (root / "src" / "util.py").write_text("pass\n") data = _status(root) assert "src/" not in data["untracked"], ( "non-empty dir must not appear in untracked; its files represent it" ) def test_D7_file_in_new_dir_in_untracked( self, code_repo: pathlib.Path ) -> None: """D7: file inside a new dir appears in untracked, dir itself does not.""" root = code_repo (root / "src").mkdir() (root / "src" / "util.py").write_text("pass\n") data = _status(root) assert "src/util.py" in data["untracked"] assert "src/" not in data["untracked"] def test_D8_clean_repo_has_no_dir_entries( self, code_repo: pathlib.Path ) -> None: """D8: clean repo has no trailing-slash entries in any bucket.""" data = _status(code_repo) def has_dir_entry(val: list[str] | Mapping[str, object] | str | bool | int | None) -> bool: if isinstance(val, list): return any(isinstance(v, str) and v.endswith("/") for v in val) if isinstance(val, dict): return any(has_dir_entry(v) for v in val.values()) return False assert not has_dir_entry(data["added"]) assert not has_dir_entry(data["deleted"]) assert not has_dir_entry(data["staged"]) assert not has_dir_entry(data["untracked"]) def test_D9_committed_dir_not_in_staged_added_after_code_add( self, repo_with_committed_dir: pathlib.Path ) -> None: """D9: running muse code add . after committing an empty dir must not show that dir as staged.added — it is already committed.""" root = repo_with_committed_dir _run(root, "code", "add", ".") data = _status(root) assert "emptydir/" not in data["staged"]["added"], ( "already-committed empty dir must not appear in staged.added after code add ." ) assert "emptydir/" not in data["added"], ( "already-committed empty dir must not appear in flat added" ) def test_D10_committed_dir_clean_after_commit( self, repo_with_committed_dir: pathlib.Path ) -> None: """D10: repo with a committed empty dir and no other changes is clean.""" data = _status(repo_with_committed_dir) assert data["clean"] is True, ( "repo with only a committed empty dir and no pending changes must be clean" ) assert data["total_changes"] == 0