gabriel / muse public
test_cmd_content_grep.py python
376 lines 13.1 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Tests for ``muse content-grep``.
2
3 Covers: no match exit-1, pattern found, --files-only, --count, --ignore-case,
4 --format json, binary skip, multi-file, stress: 100 files.
5 Working-tree mode: --working-tree searches disk, not the committed snapshot.
6 """
7
8 from __future__ import annotations
9
10 type _FileStore = dict[str, bytes]
11
12 import datetime
13 import json
14 import pathlib
15
16 import pytest
17 from tests.cli_test_helper import CliRunner
18
19 cli = None # argparse migration — CliRunner ignores this arg
20 from muse.core.object_store import write_object
21 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
22 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
23 from muse.core._types import Manifest, blob_id
24
25 runner = CliRunner()
26
27 _REPO_ID = "cgrep-test"
28
29
30 # ---------------------------------------------------------------------------
31 # Helpers
32 # ---------------------------------------------------------------------------
33
34
35 def _sha(data: bytes) -> str:
36 return blob_id(data)
37
38
39 def _init_repo(path: pathlib.Path) -> pathlib.Path:
40 muse = path / ".muse"
41 for d in ("commits", "snapshots", "objects", "refs/heads"):
42 (muse / d).mkdir(parents=True, exist_ok=True)
43 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
44 (muse / "repo.json").write_text(
45 json.dumps({"repo_id": _REPO_ID, "domain": "midi"}), encoding="utf-8"
46 )
47 return path
48
49
50 def _env(repo: pathlib.Path) -> Manifest:
51 return {"MUSE_REPO_ROOT": str(repo)}
52
53
54 _counter = 0
55
56
57 def _commit_files(root: pathlib.Path, files: _FileStore) -> str:
58 global _counter
59 _counter += 1
60 manifest: Manifest = {}
61 for rel_path, content in files.items():
62 obj_id = _sha(content)
63 write_object(root, obj_id, content)
64 manifest[rel_path] = obj_id
65 snap_id = compute_snapshot_id(manifest)
66 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
67 committed_at = datetime.datetime.now(datetime.timezone.utc)
68 commit_id = compute_commit_id(
69 repo_id=_REPO_ID,
70 parent_ids=[],
71 snapshot_id=snap_id,
72 message=f"commit {_counter}",
73 committed_at_iso=committed_at.isoformat(),
74 )
75 write_commit(root, CommitRecord(
76 commit_id=commit_id,
77 repo_id=_REPO_ID,
78 created_on_branch="main",
79 snapshot_id=snap_id,
80 message=f"commit {_counter}",
81 committed_at=committed_at,
82 ))
83 (root / ".muse" / "refs" / "heads" / "main").write_text(commit_id, encoding="utf-8")
84 return commit_id
85
86
87 # ---------------------------------------------------------------------------
88 # Unit: help
89 # ---------------------------------------------------------------------------
90
91
92 def test_content_grep_help() -> None:
93 result = runner.invoke(cli, ["content-grep", "--help"])
94 assert result.exit_code == 0
95 assert "pattern" in result.output
96
97
98 # ---------------------------------------------------------------------------
99 # Unit: no match → exit 1
100 # ---------------------------------------------------------------------------
101
102
103 def test_content_grep_no_match(tmp_path: pathlib.Path) -> None:
104 _init_repo(tmp_path)
105 _commit_files(tmp_path, {"song.txt": b"chord: Am\ntempo: 120\n"})
106 result = runner.invoke(cli, ["content-grep", "ZZZNOMATCH", "--json"], env=_env(tmp_path))
107 assert result.exit_code != 0
108 # --json must always emit valid JSON even on no-match so agents can parse safely.
109 data = json.loads(result.output)
110 assert data["total_matches"] == 0
111 assert data["results"] == []
112
113
114 # ---------------------------------------------------------------------------
115 # Unit: match found → exit 0
116 # ---------------------------------------------------------------------------
117
118
119 def test_content_grep_match_found(tmp_path: pathlib.Path) -> None:
120 _init_repo(tmp_path)
121 _commit_files(tmp_path, {"song.txt": b"chord: Cm7\ntempo: 120\n"})
122 result = runner.invoke(cli, ["content-grep", "Cm7"], env=_env(tmp_path))
123 assert result.exit_code == 0
124 assert "song.txt" in result.output
125
126
127 # ---------------------------------------------------------------------------
128 # Unit: --ignore-case
129 # ---------------------------------------------------------------------------
130
131
132 def test_content_grep_ignore_case(tmp_path: pathlib.Path) -> None:
133 _init_repo(tmp_path)
134 _commit_files(tmp_path, {"notes.txt": b"VERSE: intro melody\n"})
135 result = runner.invoke(
136 cli, ["content-grep", "verse", "--ignore-case"], env=_env(tmp_path)
137 )
138 assert result.exit_code == 0
139 assert "notes.txt" in result.output
140
141
142 def test_content_grep_case_sensitive_no_match(tmp_path: pathlib.Path) -> None:
143 _init_repo(tmp_path)
144 _commit_files(tmp_path, {"notes.txt": b"VERSE: intro melody\n"})
145 result = runner.invoke(
146 cli, ["content-grep", "verse"], env=_env(tmp_path)
147 )
148 # Case-sensitive: "verse" ≠ "VERSE" → no match.
149 assert result.exit_code != 0
150
151
152 # ---------------------------------------------------------------------------
153 # Unit: --files-only
154 # ---------------------------------------------------------------------------
155
156
157 def test_content_grep_files_only(tmp_path: pathlib.Path) -> None:
158 _init_repo(tmp_path)
159 _commit_files(tmp_path, {
160 "a.txt": b"match here\n",
161 "b.txt": b"match here too\n",
162 })
163 result = runner.invoke(
164 cli, ["content-grep", "match", "--files-only"], env=_env(tmp_path)
165 )
166 assert result.exit_code == 0
167 lines = [l.strip() for l in result.output.strip().split("\n") if l.strip()]
168 for line in lines:
169 assert ":" not in line or line.startswith("a.txt") or line.startswith("b.txt")
170
171
172 # ---------------------------------------------------------------------------
173 # Unit: --count
174 # ---------------------------------------------------------------------------
175
176
177 def test_content_grep_count(tmp_path: pathlib.Path) -> None:
178 _init_repo(tmp_path)
179 _commit_files(tmp_path, {"multi.txt": b"hit\nhit\nhit\nmiss\n"})
180 result = runner.invoke(
181 cli, ["content-grep", "hit", "--count"], env=_env(tmp_path)
182 )
183 assert result.exit_code == 0
184 assert "3" in result.output
185
186
187 # ---------------------------------------------------------------------------
188 # Unit: --format json
189 # ---------------------------------------------------------------------------
190
191
192 def test_content_grep_json_output(tmp_path: pathlib.Path) -> None:
193 _init_repo(tmp_path)
194 _commit_files(tmp_path, {"song.midi.txt": b"note: C4\nnote: D4\n"})
195 result = runner.invoke(
196 cli, ["content-grep", "note", "--json"], env=_env(tmp_path)
197 )
198 assert result.exit_code == 0
199 data = json.loads(result.output)
200 assert isinstance(data, dict)
201 assert len(data["results"]) >= 1
202 assert data["results"][0]["match_count"] >= 2
203
204
205 # ---------------------------------------------------------------------------
206 # Unit: binary file skipped silently
207 # ---------------------------------------------------------------------------
208
209
210 def test_content_grep_binary_skipped(tmp_path: pathlib.Path) -> None:
211 _init_repo(tmp_path)
212 binary_content = b"\x00\x01\x02\x03" * 100
213 text_content = b"searchable text here\n"
214 _commit_files(tmp_path, {
215 "binary.bin": binary_content,
216 "text.txt": text_content,
217 })
218 result = runner.invoke(
219 cli, ["content-grep", "searchable"], env=_env(tmp_path)
220 )
221 assert result.exit_code == 0
222 assert "text.txt" in result.output
223
224
225 # ---------------------------------------------------------------------------
226 # Unit: short flags work
227 # ---------------------------------------------------------------------------
228
229
230 def test_content_grep_short_flags(tmp_path: pathlib.Path) -> None:
231 _init_repo(tmp_path)
232 _commit_files(tmp_path, {"f.txt": b"hello world\n"})
233 result = runner.invoke(
234 cli, ["content-grep", "hello", "-i", "--json"], env=_env(tmp_path)
235 )
236 assert result.exit_code == 0
237 data = json.loads(result.output)
238 assert len(data["results"]) >= 1
239
240
241 # ---------------------------------------------------------------------------
242 # Stress: 100 files, pattern matches 50
243 # ---------------------------------------------------------------------------
244
245
246 def test_content_grep_stress_100_files(tmp_path: pathlib.Path) -> None:
247 _init_repo(tmp_path)
248 files: _FileStore = {}
249 for i in range(100):
250 content = b"TARGET_LINE\n" if i % 2 == 0 else b"other content\n"
251 files[f"file_{i:04d}.txt"] = content
252 _commit_files(tmp_path, files)
253 result = runner.invoke(
254 cli, ["content-grep", "TARGET_LINE", "--json"], env=_env(tmp_path)
255 )
256 assert result.exit_code == 0
257 data = json.loads(result.output)
258 assert len(data["results"]) == 50
259
260
261 # ---------------------------------------------------------------------------
262 # Working-tree mode: --working-tree searches disk, not the committed snapshot
263 # ---------------------------------------------------------------------------
264
265
266 def test_content_grep_working_tree_finds_uncommitted_edit(tmp_path: pathlib.Path) -> None:
267 """--working-tree finds content written to disk that is not yet committed."""
268 _init_repo(tmp_path)
269 # Commit a file with one pattern.
270 _commit_files(tmp_path, {"song.txt": b"chord: Am\n"})
271 # Write an uncommitted edit with a different pattern.
272 (tmp_path / "song.txt").write_bytes(b"chord: WORKING_TREE_ONLY\n")
273
274 # Without --working-tree, finds the committed content.
275 result_committed = runner.invoke(
276 cli, ["content-grep", "Am"], env=_env(tmp_path)
277 )
278 assert result_committed.exit_code == 0
279
280 # With --working-tree, finds the disk content.
281 result_wt = runner.invoke(
282 cli, ["content-grep", "WORKING_TREE_ONLY", "--working-tree"],
283 env=_env(tmp_path),
284 )
285 assert result_wt.exit_code == 0
286 assert "song.txt" in result_wt.output
287
288
289 def test_content_grep_working_tree_no_match(tmp_path: pathlib.Path) -> None:
290 """--working-tree returns exit 1 when pattern absent; --json still emits valid JSON."""
291 _init_repo(tmp_path)
292 (tmp_path / "notes.txt").write_bytes(b"hello world\n")
293 result = runner.invoke(
294 cli, ["content-grep", "ZZZNOMATCH", "--working-tree", "--json"],
295 env=_env(tmp_path),
296 )
297 assert result.exit_code != 0
298 data = json.loads(result.output)
299 assert data["total_matches"] == 0
300 assert data["results"] == []
301
302
303 def test_content_grep_working_tree_skips_muse_dir(tmp_path: pathlib.Path) -> None:
304 """--working-tree never searches inside the .muse object store."""
305 _init_repo(tmp_path)
306 # Write a matching string inside .muse/ — must NOT be found.
307 (tmp_path / ".muse" / "stray.txt").write_bytes(b"SECRET_IN_MUSE\n")
308 # Write the same string outside .muse/ — must be found.
309 (tmp_path / "real.txt").write_bytes(b"SECRET_IN_MUSE\n")
310
311 result = runner.invoke(
312 cli, ["content-grep", "SECRET_IN_MUSE", "--working-tree", "--json"],
313 env=_env(tmp_path),
314 )
315 assert result.exit_code == 0
316 data = json.loads(result.output)
317 paths = [r["path"] for r in data["results"]]
318 assert "real.txt" in paths
319 assert not any(".muse" in p for p in paths)
320
321
322 def test_content_grep_working_tree_json_schema(tmp_path: pathlib.Path) -> None:
323 """--working-tree JSON output has source=working-tree and null commit_id/snapshot_id."""
324 _init_repo(tmp_path)
325 (tmp_path / "f.txt").write_bytes(b"TARGET\n")
326 result = runner.invoke(
327 cli, ["content-grep", "TARGET", "--working-tree", "--json"],
328 env=_env(tmp_path),
329 )
330 assert result.exit_code == 0
331 data = json.loads(result.output)
332 assert data["source"] == "working-tree"
333 assert data["commit_id"] is None
334 assert data["snapshot_id"] is None
335 assert data["results"][0]["object_id"] is None
336
337
338 def test_content_grep_working_tree_files_only(tmp_path: pathlib.Path) -> None:
339 """--working-tree --files-only prints only file paths, no line numbers."""
340 _init_repo(tmp_path)
341 (tmp_path / "a.txt").write_bytes(b"match\n")
342 (tmp_path / "b.txt").write_bytes(b"match\n")
343 result = runner.invoke(
344 cli, ["content-grep", "match", "--working-tree", "--files-only"],
345 env=_env(tmp_path),
346 )
347 assert result.exit_code == 0
348 lines = [l.strip() for l in result.output.strip().splitlines() if l.strip()]
349 assert all(":" not in l for l in lines)
350 assert {"a.txt", "b.txt"}.issubset(set(lines))
351
352
353 def test_content_grep_working_tree_and_ref_mutually_exclusive(tmp_path: pathlib.Path) -> None:
354 """Passing both --working-tree and --ref is a user error (exit non-zero)."""
355 _init_repo(tmp_path)
356 _commit_files(tmp_path, {"f.txt": b"content\n"})
357 result = runner.invoke(
358 cli,
359 ["content-grep", "content", "--working-tree", "--ref", "main"],
360 env=_env(tmp_path),
361 )
362 assert result.exit_code != 0
363
364
365 def test_content_grep_snapshot_json_has_source_commit(tmp_path: pathlib.Path) -> None:
366 """Snapshot mode JSON output has source=commit and non-null commit_id/snapshot_id."""
367 _init_repo(tmp_path)
368 _commit_files(tmp_path, {"f.txt": b"TARGET\n"})
369 result = runner.invoke(
370 cli, ["content-grep", "TARGET", "--json"], env=_env(tmp_path)
371 )
372 assert result.exit_code == 0
373 data = json.loads(result.output)
374 assert data["source"] == "commit"
375 assert data["commit_id"] is not None
376 assert data["snapshot_id"] is not None
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago