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