gabriel / muse public

test_diff_pathspec.py file-level

at dev · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:e fix: get_signing_identity's MUSE_AGENT_KEY_FD path derived with entity_… · gabriel · Oct 22, 2026
1 """TDD — muse#36: muse diff treats a bare path as a commit ref instead of a pathspec.
2
3 Covers the ticket's acceptance criteria:
4 - `muse diff <path>` scopes by path when the token is not a valid ref but is
5 a tracked path.
6 - `muse diff -- <path>` (argparse consumes the literal "--" before run() ever
7 sees args, so this exercises the identical code path as the bare case).
8 - Ref-vs-path ambiguity: a token that is both a valid ref and a tracked path
9 resolves as a ref (the documented, sanctioned alternative when a literal
10 "--" can't be observed after argparse parsing).
11 - Consistent exit code for a token that resolves as neither ref nor path.
12 - --path/-p unaffected (out of scope per the ticket — must keep working).
13 """
14 from __future__ import annotations
15
16 import json
17 import os
18 import pathlib
19
20 import pytest
21
22 from tests.cli_test_helper import CliRunner, InvokeResult
23
24 runner = CliRunner()
25
26
27 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
28 saved = os.getcwd()
29 try:
30 os.chdir(repo)
31 return runner.invoke(None, args)
32 finally:
33 os.chdir(saved)
34
35
36 def _diff(repo: pathlib.Path, *extra: str) -> InvokeResult:
37 return _invoke(repo, ["diff", *extra])
38
39
40 def _commit(repo: pathlib.Path, msg: str) -> InvokeResult:
41 _invoke(repo, ["code", "add", "."])
42 return _invoke(repo, ["commit", "-m", msg])
43
44
45 @pytest.fixture()
46 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
47 """Repo with one commit and an uncommitted change to a.py, plus sub/b.py."""
48 saved = os.getcwd()
49 try:
50 os.chdir(tmp_path)
51 runner.invoke(None, ["init"])
52 finally:
53 os.chdir(saved)
54 (tmp_path / "a.py").write_text("x = 1\n")
55 (tmp_path / "sub").mkdir()
56 (tmp_path / "sub" / "b.py").write_text("y = 1\n")
57 _commit(tmp_path, "first")
58 (tmp_path / "a.py").write_text("x = 2\n") # uncommitted change
59 return tmp_path
60
61
62 class TestBarePathScoping:
63 def test_bare_path_scopes_diff_instead_of_erroring(self, repo: pathlib.Path) -> None:
64 result = _diff(repo, "a.py", "--json")
65 assert result.exit_code == 0, result.output
66 data = json.loads(result.output)
67 assert data["modified"] == ["a.py"]
68
69 def test_bare_path_matches_text_output_of_explicit_path_flag(
70 self, repo: pathlib.Path
71 ) -> None:
72 bare = _diff(repo, "a.py")
73 flagged = _diff(repo, "--path", "a.py")
74 assert bare.exit_code == 0
75 assert bare.output == flagged.output
76
77 def test_bare_directory_path_scopes_by_prefix(self, repo: pathlib.Path) -> None:
78 (repo / "sub" / "b.py").write_text("y = 2\n")
79 result = _diff(repo, "sub", "--json")
80 assert result.exit_code == 0, result.output
81 data = json.loads(result.output)
82 assert data["modified"] == ["sub/b.py"]
83
84 def test_bare_path_with_no_changes_reports_no_changes(
85 self, repo: pathlib.Path
86 ) -> None:
87 result = _diff(repo, "sub/b.py", "--json")
88 assert result.exit_code == 0, result.output
89 data = json.loads(result.output)
90 assert data["has_changes"] is False
91
92
93 class TestDashDashSeparator:
94 def test_dash_dash_path_scopes_diff(self, repo: pathlib.Path) -> None:
95 result = _diff(repo, "--", "a.py")
96 assert result.exit_code == 0, result.output
97
98 def test_dash_dash_path_json_matches_bare_path(self, repo: pathlib.Path) -> None:
99 bare = _diff(repo, "a.py", "--json")
100 dashdash = _diff(repo, "--json", "--", "a.py")
101 assert bare.exit_code == dashdash.exit_code == 0
102 assert json.loads(bare.output)["modified"] == json.loads(dashdash.output)["modified"]
103
104
105 class TestRefVsPathAmbiguity:
106 def test_unresolvable_token_still_errors_consistently(self, repo: pathlib.Path) -> None:
107 """A token that is neither a valid ref nor a tracked path must still
108 fail clearly — this is NOT a silent no-op."""
109 result = _diff(repo, "totally-unknown-thing")
110 assert result.exit_code != 0
111
112 def test_valid_ref_still_takes_precedence_over_same_named_path(
113 self, repo: pathlib.Path
114 ) -> None:
115 """Documented divergence: when a token is BOTH a valid ref and a
116 tracked path, muse always prefers the ref (git would require `--`
117 to force path interpretation; muse can't observe `--` after argparse
118 has consumed it, so ref wins deterministically instead)."""
119 _invoke(repo, ["branch", "a.py"]) # branch literally named "a.py"
120 result = _diff(repo, "a.py", "--json")
121 assert result.exit_code == 0, result.output
122 data = json.loads(result.output)
123 assert data["to_ref"] == "a.py"
124 # Ref interpretation: HEAD vs branch "a.py" (same commit) -> no changes,
125 # NOT a path-scoped working-tree diff (which would show has_changes=True).
126 assert data["has_changes"] is False
127
128 def test_exit_code_consistent_for_unresolvable_single_positional(
129 self, repo: pathlib.Path
130 ) -> None:
131 """Both the plain and text-mode invocations of an unresolvable token
132 exit identically (muse#36 problem 3: was inconsistent before)."""
133 plain = _diff(repo, "nonexistent-ref-or-path")
134 dashdash = _diff(repo, "--", "nonexistent-ref-or-path")
135 assert plain.exit_code == dashdash.exit_code
136 assert plain.exit_code != 0
137
138
139 class TestPathFlagUnaffected:
140 """Out of scope per the ticket: --path/-p must keep working unchanged."""
141
142 def test_path_flag_still_works(self, repo: pathlib.Path) -> None:
143 result = _diff(repo, "--path", "a.py", "--json")
144 assert result.exit_code == 0, result.output
145 assert json.loads(result.output)["modified"] == ["a.py"]
146
147 def test_short_path_flag_still_works(self, repo: pathlib.Path) -> None:
148 result = _diff(repo, "-p", "a.py", "--json")
149 assert result.exit_code == 0, result.output
150 assert json.loads(result.output)["modified"] == ["a.py"]
151
152
153 class TestDefaultAndTwoRefFormsUnaffected:
154 def test_no_args_default_diff_unaffected(self, repo: pathlib.Path) -> None:
155 result = _diff(repo, "--json")
156 assert result.exit_code == 0, result.output
157 assert json.loads(result.output)["modified"] == ["a.py"]
158
159 def test_two_ref_form_unaffected(self, repo: pathlib.Path) -> None:
160 result = _diff(repo, "HEAD", "HEAD", "--json")
161 assert result.exit_code == 0, result.output
162 assert json.loads(result.output)["has_changes"] is False
163
164 def test_single_valid_ref_unaffected(self, repo: pathlib.Path) -> None:
165 """A bare positional that IS a real ref keeps its existing
166 HEAD-vs-that-commit meaning — this is the 100%-unchanged case."""
167 result = _diff(repo, "HEAD", "--json")
168 assert result.exit_code == 0, result.output
169 data = json.loads(result.output)
170 assert data["to_ref"] == "HEAD"
171 assert data["has_changes"] is False