"""Tests for muse read — inspect a commit's metadata, diff, and files.""" import json import pathlib import pytest from tests.cli_test_helper import CliRunner from muse.core.paths import config_toml_path cli = None # argparse migration — CliRunner ignores this arg runner = CliRunner() @pytest.fixture def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: monkeypatch.chdir(tmp_path) monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) result = runner.invoke(cli, ["init"]) assert result.exit_code == 0, result.output return tmp_path def _write(repo: pathlib.Path, filename: str, content: str = "data") -> None: (repo / filename).write_text(content) def _commit(msg: str = "initial", **flags: str) -> str: runner.invoke(cli, ["code", "add", "."]) args = ["commit", "-m", msg] for k, v in flags.items(): args += [f"--{k}", v] result = runner.invoke(cli, args) assert result.exit_code == 0, result.output # output: "[main abcd1234] msg" → strip trailing ] from token return result.output.split()[1].rstrip("]") class TestShowHead: def test_shows_commit_id(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") _commit("initial commit") result = runner.invoke(cli, ["read"]) assert result.exit_code == 0, result.output assert "commit" in result.output def test_shows_message(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") _commit("my special message") result = runner.invoke(cli, ["read"]) assert result.exit_code == 0 assert "my special message" in result.output def test_shows_date(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") _commit("dated commit") result = runner.invoke(cli, ["read"]) assert result.exit_code == 0 assert "Date:" in result.output def test_shows_author(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") runner.invoke(cli, ["commit", "-m", "authored", "--author", "Gabriel"]) result = runner.invoke(cli, ["read"]) assert result.exit_code == 0 assert "Gabriel" in result.output def test_author_from_identity_when_not_explicit(self, repo: pathlib.Path) -> None: import muse.core.identity as _id_mod # Wire a hub URL so get_config_value("user.handle") can resolve. (config_toml_path(repo)).write_text('[hub]\nurl = "https://localhost:1337"\n') # Write a minimal identity entry for that hub (patched path via conftest). _id_mod._IDENTITY_FILE.parent.mkdir(parents=True, exist_ok=True) _id_mod._IDENTITY_FILE.write_text( '["localhost:1337"]\nhandle = "gabriel"\ntype = "human"\nalgorithm = "ed25519"\n' ) _write(repo, "beat.py") _commit("implicit author") result = runner.invoke(cli, ["read"]) assert result.exit_code == 0 assert "Author:" in result.output class TestShowStat: def test_shows_added_file_by_default(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") _commit("add beat") result = runner.invoke(cli, ["read"]) assert result.exit_code == 0 assert "beat.py" in result.output assert "+" in result.output def test_no_stat_flag_hides_files(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") _commit("add beat") result = runner.invoke(cli, ["read", "--no-stat"]) assert result.exit_code == 0 assert "beat.py" not in result.output def test_shows_modified_file(self, repo: pathlib.Path) -> None: _write(repo, "beat.py", "v1") _commit("v1") _write(repo, "beat.py", "v2") _commit("v2") result = runner.invoke(cli, ["read"]) assert result.exit_code == 0 assert "beat.py" in result.output def test_file_change_count(self, repo: pathlib.Path) -> None: _write(repo, "a.py") _write(repo, "b.py") _commit("two files") result = runner.invoke(cli, ["read"]) assert result.exit_code == 0 assert "added" in result.output or "file(s) changed" in result.output def test_no_files_changed_no_count_line(self, repo: pathlib.Path) -> None: _write(repo, "beat.py", "v1") _commit("v1") _write(repo, "beat.py", "v1") result = runner.invoke(cli, ["commit", "--allow-empty"]) # empty commit — stat block should show no files changed result2 = runner.invoke(cli, ["read"]) assert result2.exit_code == 0 class TestShowMetadata: def test_shows_section_metadata(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") runner.invoke(cli, ["commit", "-m", "verse", "--section", "verse"]) result = runner.invoke(cli, ["read"]) assert result.exit_code == 0 assert "section" in result.output assert "verse" in result.output def test_shows_track_and_emotion(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") runner.invoke(cli, ["commit", "-m", "drums", "--track", "drums", "--emotion", "joyful"]) result = runner.invoke(cli, ["read"]) assert result.exit_code == 0 assert "track" in result.output assert "emotion" in result.output class TestShowRef: def test_show_specific_commit(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") short = _commit("first") _write(repo, "lead.py") _commit("second") # show the first commit by prefix result = runner.invoke(cli, ["read", short]) assert result.exit_code == 0 assert "first" in result.output def test_show_unknown_ref_errors(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") _commit("only") result = runner.invoke(cli, ["read", "deadbeef"]) assert result.exit_code != 0 assert "not found" in result.stderr.lower() or "deadbeef" in result.stderr def test_show_no_commits_errors(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["read"]) assert result.exit_code != 0 class TestShowParent: def test_shows_parent_after_second_commit(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") _commit("first") _write(repo, "lead.py") _commit("second") result = runner.invoke(cli, ["read"]) assert result.exit_code == 0 assert "Parent:" in result.output def test_root_commit_has_no_parent_line(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") short = _commit("root commit") result = runner.invoke(cli, ["read", short]) assert result.exit_code == 0 assert "Parent:" not in result.output class TestShowJson: def test_json_output_is_valid(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") _commit("json test") result = runner.invoke(cli, ["read", "--json"]) assert result.exit_code == 0 data = json.loads(result.output) assert "commit_id" in data assert "message" in data def test_json_contains_message(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") _commit("the message") result = runner.invoke(cli, ["read", "--json"]) data = json.loads(result.output) assert data["message"] == "the message" def test_json_with_stat_includes_file_lists(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") _commit("add beat") result = runner.invoke(cli, ["read", "--json"]) data = json.loads(result.output) assert "files_added" in data assert "beat.py" in data["files_added"] def test_json_no_stat_excludes_file_lists(self, repo: pathlib.Path) -> None: _write(repo, "beat.py") _commit("add beat") result = runner.invoke(cli, ["read", "--json", "--no-stat"]) data = json.loads(result.output) assert "files_added" not in data def test_json_stat_shows_removed_file(self, repo: pathlib.Path) -> None: _write(repo, "beat.py", "v1") _commit("add") (repo / "beat.py").unlink() _write(repo, "lead.py", "new") _commit("swap") result = runner.invoke(cli, ["read", "--json"]) data = json.loads(result.output) assert "beat.py" in data["files_removed"] def test_json_stat_shows_modified_file(self, repo: pathlib.Path) -> None: _write(repo, "beat.py", "v1") _commit("v1") _write(repo, "beat.py", "v2") _commit("v2") result = runner.invoke(cli, ["read", "--json"]) data = json.loads(result.output) assert "beat.py" in data["files_modified"]