"""TDD — muse#36: muse diff treats a bare path as a commit ref instead of a pathspec. Covers the ticket's acceptance criteria: - `muse diff ` scopes by path when the token is not a valid ref but is a tracked path. - `muse diff -- ` (argparse consumes the literal "--" before run() ever sees args, so this exercises the identical code path as the bare case). - Ref-vs-path ambiguity: a token that is both a valid ref and a tracked path resolves as a ref (the documented, sanctioned alternative when a literal "--" can't be observed after argparse parsing). - Consistent exit code for a token that resolves as neither ref nor path. - --path/-p unaffected (out of scope per the ticket — must keep working). """ from __future__ import annotations import json import os import pathlib import pytest from tests.cli_test_helper import CliRunner, InvokeResult runner = CliRunner() def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult: saved = os.getcwd() try: os.chdir(repo) return runner.invoke(None, args) finally: os.chdir(saved) def _diff(repo: pathlib.Path, *extra: str) -> InvokeResult: return _invoke(repo, ["diff", *extra]) def _commit(repo: pathlib.Path, msg: str) -> InvokeResult: _invoke(repo, ["code", "add", "."]) return _invoke(repo, ["commit", "-m", msg]) @pytest.fixture() def repo(tmp_path: pathlib.Path) -> pathlib.Path: """Repo with one commit and an uncommitted change to a.py, plus sub/b.py.""" saved = os.getcwd() try: os.chdir(tmp_path) runner.invoke(None, ["init"]) finally: os.chdir(saved) (tmp_path / "a.py").write_text("x = 1\n") (tmp_path / "sub").mkdir() (tmp_path / "sub" / "b.py").write_text("y = 1\n") _commit(tmp_path, "first") (tmp_path / "a.py").write_text("x = 2\n") # uncommitted change return tmp_path class TestBarePathScoping: def test_bare_path_scopes_diff_instead_of_erroring(self, repo: pathlib.Path) -> None: result = _diff(repo, "a.py", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["modified"] == ["a.py"] def test_bare_path_matches_text_output_of_explicit_path_flag( self, repo: pathlib.Path ) -> None: bare = _diff(repo, "a.py") flagged = _diff(repo, "--path", "a.py") assert bare.exit_code == 0 assert bare.output == flagged.output def test_bare_directory_path_scopes_by_prefix(self, repo: pathlib.Path) -> None: (repo / "sub" / "b.py").write_text("y = 2\n") result = _diff(repo, "sub", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["modified"] == ["sub/b.py"] def test_bare_path_with_no_changes_reports_no_changes( self, repo: pathlib.Path ) -> None: result = _diff(repo, "sub/b.py", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["has_changes"] is False class TestDashDashSeparator: def test_dash_dash_path_scopes_diff(self, repo: pathlib.Path) -> None: result = _diff(repo, "--", "a.py") assert result.exit_code == 0, result.output def test_dash_dash_path_json_matches_bare_path(self, repo: pathlib.Path) -> None: bare = _diff(repo, "a.py", "--json") dashdash = _diff(repo, "--json", "--", "a.py") assert bare.exit_code == dashdash.exit_code == 0 assert json.loads(bare.output)["modified"] == json.loads(dashdash.output)["modified"] class TestRefVsPathAmbiguity: def test_unresolvable_token_still_errors_consistently(self, repo: pathlib.Path) -> None: """A token that is neither a valid ref nor a tracked path must still fail clearly — this is NOT a silent no-op.""" result = _diff(repo, "totally-unknown-thing") assert result.exit_code != 0 def test_valid_ref_still_takes_precedence_over_same_named_path( self, repo: pathlib.Path ) -> None: """Documented divergence: when a token is BOTH a valid ref and a tracked path, muse always prefers the ref (git would require `--` to force path interpretation; muse can't observe `--` after argparse has consumed it, so ref wins deterministically instead).""" _invoke(repo, ["branch", "a.py"]) # branch literally named "a.py" result = _diff(repo, "a.py", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["to_ref"] == "a.py" # Ref interpretation: HEAD vs branch "a.py" (same commit) -> no changes, # NOT a path-scoped working-tree diff (which would show has_changes=True). assert data["has_changes"] is False def test_exit_code_consistent_for_unresolvable_single_positional( self, repo: pathlib.Path ) -> None: """Both the plain and text-mode invocations of an unresolvable token exit identically (muse#36 problem 3: was inconsistent before).""" plain = _diff(repo, "nonexistent-ref-or-path") dashdash = _diff(repo, "--", "nonexistent-ref-or-path") assert plain.exit_code == dashdash.exit_code assert plain.exit_code != 0 class TestPathFlagUnaffected: """Out of scope per the ticket: --path/-p must keep working unchanged.""" def test_path_flag_still_works(self, repo: pathlib.Path) -> None: result = _diff(repo, "--path", "a.py", "--json") assert result.exit_code == 0, result.output assert json.loads(result.output)["modified"] == ["a.py"] def test_short_path_flag_still_works(self, repo: pathlib.Path) -> None: result = _diff(repo, "-p", "a.py", "--json") assert result.exit_code == 0, result.output assert json.loads(result.output)["modified"] == ["a.py"] class TestDefaultAndTwoRefFormsUnaffected: def test_no_args_default_diff_unaffected(self, repo: pathlib.Path) -> None: result = _diff(repo, "--json") assert result.exit_code == 0, result.output assert json.loads(result.output)["modified"] == ["a.py"] def test_two_ref_form_unaffected(self, repo: pathlib.Path) -> None: result = _diff(repo, "HEAD", "HEAD", "--json") assert result.exit_code == 0, result.output assert json.loads(result.output)["has_changes"] is False def test_single_valid_ref_unaffected(self, repo: pathlib.Path) -> None: """A bare positional that IS a real ref keeps its existing HEAD-vs-that-commit meaning — this is the 100%-unchanged case.""" result = _diff(repo, "HEAD", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["to_ref"] == "HEAD" assert data["has_changes"] is False