gabriel / muse public
test_cmd_code_query.py python
1,136 lines 45.2 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 121 days ago
1 """Comprehensive tests for ``muse code code-query``.
2
3 Review findings addressed
4 --------------------------
5 Bug fixes
6 * ``walk_history`` used ``ref_file.read_text()`` directly instead of
7 ``get_head_commit_id`` — now correctly delegates to the store.
8 * The redundant double-check ``op_rec.get("op") == "patch" and op_rec["op"] == "patch"``
9 removed; replaced with ``_is_patch_op`` TypeGuard.
10 * Dead ``if field_val is not None`` check (``field_val`` is always a ``str``) removed.
11 * Dead ``_current_branch`` wrapper removed from CLI; uses ``read_current_branch`` directly.
12 * Double-pass evaluator fallback for commit-level fields replaced with a single
13 clean pass using an ``or_matched`` flag.
14 * Redundant ``list(matches)`` call in JSON output removed.
15
16 New capabilities
17 * ``endswith`` operator added to DSL and evaluator.
18 * ``--since DATE`` / ``--until DATE`` time-range filters.
19 * ``--limit N`` result cap (independent of ``--max`` walk depth).
20 * ``--count`` flag: prints only the match count.
21 * ``load_manifest=False`` in ``walk_history``: skips snapshot I/O for code queries.
22 * ``walk_history`` now uses ``get_head_commit_id`` instead of reading ref file directly.
23
24 Test categories
25 ---------------
26 P Parser — all operators, fields, quoted/unquoted, error paths.
27 E Evaluator (unit) — match/no-match for all operators and field types.
28 W walk_history integration — load_manifest optimisation, since/until,
29 max_commits, empty branch, multi-commit ordering.
30 C CLI E2E — --count, --limit, --since, --until, --json, bad input.
31 S Stress — 300-commit walk, large OR expression, no-manifest I/O path.
32 """
33
34 from __future__ import annotations
35
36 import argparse
37 import datetime
38 import json
39 import pathlib
40 from collections.abc import Generator
41 from unittest.mock import MagicMock, patch
42
43 import pytest
44
45 from muse.core.query_engine import QueryMatch, format_matches, walk_history
46 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
47 from muse.core.store import (
48 CommitRecord,
49 write_commit,
50 )
51 from muse.domain import DeleteOp, DomainOp, InsertOp, PatchOp, ReplaceOp, SemVerBump, StructuredDelta
52
53 from muse.core.types import Manifest, NULL_COMMIT_ID, NULL_LONG_ID, fake_id
54 from muse.plugins.code._code_query import (
55 AndExpr,
56 Comparison,
57 OrExpr,
58 _match_op,
59 _parse_query,
60 build_evaluator,
61 )
62 from muse.core.paths import commits_dir, head_path, heads_dir, muse_dir
63 from tests.cli_test_helper import CliRunner
64
65 runner = CliRunner()
66 cli = None
67
68
69 # ---------------------------------------------------------------------------
70 # Helpers
71 # ---------------------------------------------------------------------------
72
73
74 def _env(root: pathlib.Path) -> Manifest:
75 return {"MUSE_REPO_ROOT": str(root)}
76
77
78 def _run(root: pathlib.Path, *args: str) -> tuple[int, str]:
79 result = runner.invoke(cli, list(args), env=_env(root), catch_exceptions=False)
80 return result.exit_code, result.output
81
82
83 def _run_unchecked(root: pathlib.Path, *args: str) -> tuple[int, str]:
84 result = runner.invoke(cli, list(args), env=_env(root))
85 return result.exit_code, result.output
86
87
88
89 def _now() -> datetime.datetime:
90 return datetime.datetime.now(datetime.timezone.utc)
91
92
93 def _dt(year: int = 2026, month: int = 3, day: int = 1) -> datetime.datetime:
94 return datetime.datetime(year, month, day, tzinfo=datetime.timezone.utc)
95
96
97 def _insert_delta(*symbols: str, file: str = "src/foo.py") -> StructuredDelta:
98 ops: list[DomainOp] = [
99 InsertOp(
100 op="insert",
101 address=f"{file}::{sym}",
102 position=None,
103 content_id=fake_id(sym),
104 content_summary=f"added {sym}",
105 )
106 for sym in symbols
107 ]
108 return StructuredDelta(domain="code", ops=ops, summary=f"{len(ops)} symbol(s) added")
109
110
111 def _delete_delta(symbol: str, file: str = "src/foo.py") -> StructuredDelta:
112 op = DeleteOp(
113 op="delete",
114 address=f"{file}::{symbol}",
115 content_id=fake_id(symbol),
116 position=None,
117 content_summary=f"deleted {symbol}",
118 )
119 return StructuredDelta(domain="code", ops=[op], summary="1 symbol deleted")
120
121
122 def _make_commit(
123 root: pathlib.Path,
124 branch: str = "main",
125 parent: str | None = None,
126 delta: StructuredDelta | None = None,
127 author: str = "alice",
128 agent_id: str = "",
129 model_id: str = "",
130 sem_ver_bump: SemVerBump = "none",
131 committed_at: datetime.datetime | None = None,
132 message: str = "test commit",
133 ) -> CommitRecord:
134 """Write a CommitRecord with a content-addressed ID to *root* and return it."""
135 snap_id = compute_snapshot_id({})
136 committed_at_val = committed_at or _now()
137 parent_ids = [parent] if parent else []
138 commit_id = compute_commit_id( parent_ids=parent_ids,
139 snapshot_id=snap_id,
140 message=message,
141 committed_at_iso=committed_at_val.isoformat(),
142 author=author,
143 )
144 rec = CommitRecord(
145 commit_id=commit_id,
146 repo_id="test-repo",
147 branch=branch,
148 snapshot_id=snap_id,
149 message=message,
150 committed_at=committed_at_val,
151 parent_commit_id=parent,
152 author=author,
153 agent_id=agent_id,
154 model_id=model_id,
155 sem_ver_bump=sem_ver_bump,
156 structured_delta=delta,
157 )
158 write_commit(root, rec)
159 return rec
160
161
162 def _setup_branch(
163 root: pathlib.Path,
164 branch: str = "main",
165 commits: list[CommitRecord] | None = None,
166 ) -> None:
167 """Wire up HEAD and branch ref so walk_history can find the commits."""
168 muse_dir(root).mkdir(exist_ok=True)
169 (head_path(root)).write_text(branch)
170 refs_dir = heads_dir(root)
171 refs_dir.mkdir(parents=True, exist_ok=True)
172 if commits:
173 (refs_dir / branch).write_text(commits[-1].commit_id)
174
175
176 @pytest.fixture()
177 def store_root(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
178 """Minimal repo layout: .muse/ directories, no branch yet."""
179 (commits_dir(tmp_path)).mkdir(parents=True)
180 (heads_dir(tmp_path)).mkdir(parents=True)
181 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
182 return tmp_path
183
184
185 @pytest.fixture()
186 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
187 """Full muse-init repo for E2E CLI tests."""
188 monkeypatch.chdir(tmp_path)
189 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
190 assert r.exit_code == 0, r.output
191 return tmp_path
192
193
194 # ---------------------------------------------------------------------------
195 # P — Parser tests
196 # ---------------------------------------------------------------------------
197
198
199 class TestParser:
200 """Tokenizer and parser unit tests."""
201
202 def test_endswith_operator_parsed(self) -> None:
203 q = _parse_query("symbol endswith _handler")
204 cmp = q.clauses[0].clauses[0]
205 assert cmp.op == "endswith"
206 assert cmp.value == "_handler"
207
208 def test_all_operators_accepted(self) -> None:
209 for op in ("==", "!=", "contains", "startswith", "endswith"):
210 q = _parse_query(f"author {op} alice")
211 assert q.clauses[0].clauses[0].op == op
212
213 def test_all_valid_fields_accepted(self) -> None:
214 fields = [
215 "symbol", "file", "language", "kind", "change",
216 "author", "agent_id", "model_id", "toolchain_id",
217 "sem_ver_bump", "branch",
218 ]
219 for f in fields:
220 q = _parse_query(f"{f} == test")
221 assert q.clauses[0].clauses[0].field == f
222
223 def test_complex_and_or_query(self) -> None:
224 q = _parse_query("author == 'alice' and change == 'added' or author == 'bob'")
225 assert isinstance(q, OrExpr)
226 assert len(q.clauses) == 2
227 assert len(q.clauses[0].clauses) == 2
228 assert len(q.clauses[1].clauses) == 1
229
230 def test_single_quoted_value(self) -> None:
231 q = _parse_query("agent_id == 'claude-4'")
232 assert q.clauses[0].clauses[0].value == "claude-4"
233
234 def test_double_quoted_value(self) -> None:
235 q = _parse_query('model_id == "claude-opus-4"')
236 assert q.clauses[0].clauses[0].value == "claude-opus-4"
237
238 def test_unquoted_word_value(self) -> None:
239 q = _parse_query("branch == dev")
240 assert q.clauses[0].clauses[0].value == "dev"
241
242 def test_unknown_field_raises(self) -> None:
243 with pytest.raises(ValueError, match="Unknown field"):
244 _parse_query("nonexistent == 'x'")
245
246 def test_unknown_operator_raises(self) -> None:
247 with pytest.raises(ValueError, match="Unknown operator"):
248 _parse_query("author like alice")
249
250 def test_multiple_and_clauses(self) -> None:
251 q = _parse_query("author == 'alice' and change == 'added' and kind == 'function'")
252 assert len(q.clauses[0].clauses) == 3
253
254 def test_multiple_or_clauses(self) -> None:
255 q = _parse_query("author == 'a' or author == 'b' or author == 'c'")
256 assert len(q.clauses) == 3
257
258 def test_endswith_in_and_chain(self) -> None:
259 q = _parse_query("file endswith .py and symbol endswith _test")
260 clauses = q.clauses[0].clauses
261 assert clauses[0].op == "endswith"
262 assert clauses[1].op == "endswith"
263
264 def test_sem_ver_bump_values_accepted(self) -> None:
265 for val in ("none", "patch", "minor", "major"):
266 q = _parse_query(f"sem_ver_bump == {val}")
267 assert q.clauses[0].clauses[0].value == val
268
269
270 # ---------------------------------------------------------------------------
271 # E — Evaluator unit tests
272 # ---------------------------------------------------------------------------
273
274
275 def _bare_commit(
276 author: str = "alice",
277 agent_id: str = "",
278 model_id: str = "",
279 branch: str = "main",
280 sem_ver_bump: SemVerBump = "none",
281 delta: StructuredDelta | None = None,
282 message: str = "test",
283 ) -> CommitRecord:
284 return CommitRecord(
285 commit_id=fake_id(f"{author}|{agent_id}|{branch}"),
286 repo_id="test-repo",
287 branch=branch,
288 snapshot_id="s" * 64,
289 message=message,
290 committed_at=_now(),
291 author=author,
292 agent_id=agent_id,
293 model_id=model_id,
294 sem_ver_bump=sem_ver_bump,
295 structured_delta=delta,
296 )
297
298
299 class TestMatchOp:
300 """Unit tests for the _match_op primitive."""
301
302 def test_eq_match(self) -> None:
303 assert _match_op("alice", "==", "alice") is True
304
305 def test_eq_no_match(self) -> None:
306 assert _match_op("alice", "==", "bob") is False
307
308 def test_neq_match(self) -> None:
309 assert _match_op("alice", "!=", "bob") is True
310
311 def test_neq_no_match(self) -> None:
312 assert _match_op("alice", "!=", "alice") is False
313
314 def test_contains_case_insensitive(self) -> None:
315 assert _match_op("ClaudeBot", "contains", "claude") is True
316
317 def test_startswith_case_insensitive(self) -> None:
318 assert _match_op("Claude-opus", "startswith", "claude") is True
319
320 def test_endswith_match(self) -> None:
321 assert _match_op("my_handler", "endswith", "_handler") is True
322
323 def test_endswith_no_match(self) -> None:
324 assert _match_op("my_handler", "endswith", "_service") is False
325
326 def test_endswith_case_insensitive(self) -> None:
327 assert _match_op("MyHandler", "endswith", "handler") is True
328
329 def test_endswith_empty_suffix(self) -> None:
330 assert _match_op("anything", "endswith", "") is True
331
332
333 class TestBuildEvaluator:
334 """Evaluator closure tests."""
335
336 def test_author_eq_match(self) -> None:
337 ev = build_evaluator("author == 'alice'")
338 results = ev(_bare_commit(author="alice"), {}, pathlib.Path("."))
339 assert len(results) == 1
340
341 def test_author_eq_no_match(self) -> None:
342 ev = build_evaluator("author == 'bob'")
343 results = ev(_bare_commit(author="alice"), {}, pathlib.Path("."))
344 assert results == []
345
346 def test_author_contains(self) -> None:
347 ev = build_evaluator("author contains li")
348 results = ev(_bare_commit(author="alice"), {}, pathlib.Path("."))
349 assert len(results) == 1
350
351 def test_agent_id_contains(self) -> None:
352 ev = build_evaluator("agent_id contains claude")
353 results = ev(_bare_commit(agent_id="claude-4.6"), {}, pathlib.Path("."))
354 assert len(results) == 1
355
356 def test_model_id_startswith(self) -> None:
357 ev = build_evaluator("model_id startswith claude")
358 results = ev(_bare_commit(model_id="claude-opus-4"), {}, pathlib.Path("."))
359 assert len(results) == 1
360
361 def test_branch_match(self) -> None:
362 ev = build_evaluator("branch == dev")
363 results = ev(_bare_commit(branch="dev"), {}, pathlib.Path("."))
364 assert len(results) == 1
365
366 def test_sem_ver_bump_major(self) -> None:
367 ev = build_evaluator("sem_ver_bump == major")
368 results = ev(_bare_commit(sem_ver_bump="major"), {}, pathlib.Path("."))
369 assert len(results) == 1
370
371 def test_and_both_must_match(self) -> None:
372 ev = build_evaluator("author == 'alice' and agent_id == 'bot'")
373 commit = _bare_commit(author="alice", agent_id="human")
374 assert ev(commit, {}, pathlib.Path(".")) == []
375
376 def test_and_all_match(self) -> None:
377 ev = build_evaluator("author == 'alice' and agent_id == 'bot'")
378 commit = _bare_commit(author="alice", agent_id="bot")
379 assert len(ev(commit, {}, pathlib.Path("."))) == 1
380
381 def test_or_first_clause_matches(self) -> None:
382 ev = build_evaluator("author == 'alice' or author == 'bob'")
383 assert len(ev(_bare_commit(author="alice"), {}, pathlib.Path("."))) >= 1
384
385 def test_or_second_clause_matches(self) -> None:
386 ev = build_evaluator("author == 'alice' or author == 'bob'")
387 assert len(ev(_bare_commit(author="bob"), {}, pathlib.Path("."))) >= 1
388
389 def test_or_neither_clause_matches(self) -> None:
390 ev = build_evaluator("author == 'alice' or author == 'bob'")
391 assert ev(_bare_commit(author="carol"), {}, pathlib.Path(".")) == []
392
393 def test_symbol_eq_from_delta(self) -> None:
394 delta = _insert_delta("my_func")
395 ev = build_evaluator("symbol == 'my_func'")
396 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
397 assert len(results) >= 1
398 assert any("my_func" in r.get("detail", "") for r in results)
399
400 def test_symbol_endswith(self) -> None:
401 delta = _insert_delta("my_handler", "other_service")
402 ev = build_evaluator("symbol endswith _handler")
403 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
404 assert len(results) >= 1
405 assert all("_handler" in r.get("detail", "").lower() for r in results)
406
407 def test_symbol_endswith_no_match(self) -> None:
408 delta = _insert_delta("my_service")
409 ev = build_evaluator("symbol endswith _handler")
410 assert ev(_bare_commit(delta=delta), {}, pathlib.Path(".")) == []
411
412 def test_change_added(self) -> None:
413 delta = _insert_delta("func_a")
414 ev = build_evaluator("change == added")
415 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
416 assert len(results) >= 1
417
418 def test_change_removed(self) -> None:
419 delta = _delete_delta("old_func")
420 ev = build_evaluator("change == removed")
421 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
422 assert len(results) >= 1
423
424 def test_change_no_delta(self) -> None:
425 ev = build_evaluator("change == added")
426 assert ev(_bare_commit(delta=None), {}, pathlib.Path(".")) == []
427
428 def test_file_eq_match(self) -> None:
429 delta = _insert_delta("func", file="src/core.py")
430 ev = build_evaluator("file == 'src/core.py'")
431 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
432 assert len(results) >= 1
433
434 def test_file_contains(self) -> None:
435 delta = _insert_delta("func", file="muse/core/store.py")
436 ev = build_evaluator("file contains core")
437 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
438 assert len(results) >= 1
439
440 def test_file_endswith_extension(self) -> None:
441 delta = _insert_delta("func", file="muse/core/store.py")
442 ev = build_evaluator("file endswith .py")
443 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
444 assert len(results) >= 1
445
446 def test_language_python(self) -> None:
447 delta = _insert_delta("func", file="muse/core/store.py")
448 ev = build_evaluator("language == Python")
449 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
450 assert len(results) >= 1
451
452 def test_symbol_cap_at_20(self) -> None:
453 """Per-commit symbol match cap is 20."""
454 symbols = [f"func_{i}" for i in range(30)]
455 delta = _insert_delta(*symbols)
456 ev = build_evaluator("change == added")
457 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
458 assert len(results) == 20
459
460 def test_commit_level_match_detail_is_message(self) -> None:
461 """Commit-level match uses the commit message as detail."""
462 ev = build_evaluator("author == 'alice'")
463 commit = _bare_commit(author="alice", message="Fix the auth bug")
464 results = ev(commit, {}, pathlib.Path("."))
465 assert len(results) == 1
466 assert "Fix the auth bug" in results[0]["detail"]
467
468 def test_mixed_or_commit_level_clause_first_matches(self) -> None:
469 """OR with commit-level first clause: matching commit gets a result even without delta."""
470 ev = build_evaluator("author == 'alice' or change == 'added'")
471 commit = _bare_commit(author="alice", delta=None)
472 results = ev(commit, {}, pathlib.Path("."))
473 # alice's author clause matched; no delta → commit-level QueryMatch
474 assert len(results) == 1
475
476 def test_mixed_or_symbol_clause_second_matches(self) -> None:
477 """OR with symbol-level second clause: delta provides symbol details."""
478 delta = _insert_delta("my_func")
479 ev = build_evaluator("author == 'nobody' or change == 'added'")
480 commit = _bare_commit(author="alice", delta=delta)
481 results = ev(commit, {}, pathlib.Path("."))
482 assert len(results) >= 1
483 assert any("added" in r.get("detail", "") for r in results)
484
485 def test_patch_op_child_ops_traversed(self) -> None:
486 """PatchOp.child_ops should be evaluated for symbol matches."""
487 child: InsertOp = InsertOp(
488 op="insert",
489 address="src/module.py::child_func",
490 position=None,
491 content_id="c" * 64,
492 content_summary="child added",
493 )
494 patch_op: PatchOp = PatchOp(
495 op="patch",
496 address="src/module.py",
497 child_ops=[child],
498 child_domain="code",
499 child_summary="",
500 )
501 delta: StructuredDelta = StructuredDelta(
502 domain="code", ops=[patch_op], summary="patched module"
503 )
504 ev = build_evaluator("symbol == 'child_func'")
505 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
506 assert len(results) >= 1
507
508 def test_agent_id_in_result(self) -> None:
509 """agent_id appears in the QueryMatch when set."""
510 ev = build_evaluator("author == 'alice'")
511 commit = _bare_commit(author="alice", agent_id="claude-4.6")
512 results = ev(commit, {}, pathlib.Path("."))
513 assert results[0].get("agent_id") == "claude-4.6"
514
515 def test_agent_id_absent_when_empty(self) -> None:
516 """agent_id key is absent from QueryMatch when commit has no agent."""
517 ev = build_evaluator("author == 'alice'")
518 commit = _bare_commit(author="alice", agent_id="")
519 results = ev(commit, {}, pathlib.Path("."))
520 assert "agent_id" not in results[0]
521
522 def test_extra_dict_in_symbol_match(self) -> None:
523 """Symbol-level matches carry an 'extra' dict with file/symbol/change."""
524 delta = _insert_delta("my_func", file="src/core.py")
525 ev = build_evaluator("change == added")
526 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
527 extra = results[0].get("extra", {})
528 assert extra.get("file") == "src/core.py"
529 assert extra.get("symbol") == "my_func"
530 assert extra.get("change") == "added"
531
532
533 # ---------------------------------------------------------------------------
534 # W — walk_history integration tests
535 # ---------------------------------------------------------------------------
536
537
538 class TestWalkHistory:
539 """Integration tests that write real commit records and call walk_history."""
540
541 def test_single_commit_match(self, store_root: pathlib.Path) -> None:
542 c = _make_commit(store_root, author="alice")
543 _setup_branch(store_root, commits=[c])
544 ev = build_evaluator("author == alice")
545 results = walk_history(store_root, "main", ev, load_manifest=False)
546 assert len(results) == 1
547
548 def test_single_commit_no_match(self, store_root: pathlib.Path) -> None:
549 c = _make_commit(store_root, author="alice", message="no match commit")
550 _setup_branch(store_root, commits=[c])
551 ev = build_evaluator("author == bob")
552 results = walk_history(store_root, "main", ev, load_manifest=False)
553 assert results == []
554
555 def test_multi_commit_chained(self, store_root: pathlib.Path) -> None:
556 """Three-commit chain: all should be walked."""
557 c1 = _make_commit(store_root, author="alice", message="first")
558 c2 = _make_commit(store_root, author="alice", message="second", parent=c1.commit_id)
559 c3 = _make_commit(store_root, author="alice", message="third", parent=c2.commit_id)
560 _setup_branch(store_root, commits=[c1, c2, c3])
561 ev = build_evaluator("author == alice")
562 results = walk_history(store_root, "main", ev, load_manifest=False)
563 assert len(results) == 3
564
565 def test_max_commits_respected(self, store_root: pathlib.Path) -> None:
566 prev: str | None = None
567 commits: list[CommitRecord] = []
568 for i in range(10):
569 c = _make_commit(store_root, author="alice", parent=prev, message=f"commit {i}")
570 commits.append(c)
571 prev = c.commit_id
572 _setup_branch(store_root, commits=commits)
573 ev = build_evaluator("author == alice")
574 results = walk_history(store_root, "main", ev, max_commits=5, load_manifest=False)
575 assert len(results) == 5
576
577 def test_empty_branch_returns_empty(self, store_root: pathlib.Path) -> None:
578 # Branch ref file does not exist.
579 ev = build_evaluator("author == alice")
580 results = walk_history(store_root, "ghost", ev, load_manifest=False)
581 assert results == []
582
583 def test_load_manifest_false_skips_manifest_io(
584 self, store_root: pathlib.Path
585 ) -> None:
586 """load_manifest=False must not call get_commit_snapshot_manifest."""
587 c = _make_commit(store_root, author="alice", message="manifest skip")
588 _setup_branch(store_root, commits=[c])
589 ev = build_evaluator("author == alice")
590 with patch(
591 "muse.core.query_engine.get_commit_snapshot_manifest"
592 ) as mock_manifest:
593 walk_history(store_root, "main", ev, load_manifest=False)
594 mock_manifest.assert_not_called()
595
596 def test_load_manifest_true_calls_manifest_io(
597 self, store_root: pathlib.Path
598 ) -> None:
599 """load_manifest=True (the default) should attempt manifest loading."""
600 c = _make_commit(store_root, author="alice", message="manifest load")
601 _setup_branch(store_root, commits=[c])
602 ev = build_evaluator("author == alice")
603 with patch(
604 "muse.core.query_engine.get_commit_snapshot_manifest",
605 return_value={},
606 ) as mock_manifest:
607 walk_history(store_root, "main", ev, load_manifest=True)
608 mock_manifest.assert_called_once()
609
610 def test_since_filters_old_commits(self, store_root: pathlib.Path) -> None:
611 old = _make_commit(
612 store_root, author="alice",
613 committed_at=_dt(2025, 1, 1), message="old commit",
614 )
615 new = _make_commit(
616 store_root, author="alice",
617 committed_at=_dt(2026, 3, 1),
618 parent=old.commit_id, message="new commit",
619 )
620 _setup_branch(store_root, commits=[old, new])
621 ev = build_evaluator("author == alice")
622 results = walk_history(
623 store_root, "main", ev, load_manifest=False,
624 since=_dt(2026, 1, 1),
625 )
626 # Only the 2026 commit passes the filter.
627 assert len(results) == 1
628
629 def test_until_filters_new_commits(self, store_root: pathlib.Path) -> None:
630 old = _make_commit(
631 store_root, author="alice",
632 committed_at=_dt(2025, 6, 1), message="old until commit",
633 )
634 new = _make_commit(
635 store_root, author="alice",
636 committed_at=_dt(2026, 3, 26),
637 parent=old.commit_id, message="new until commit",
638 )
639 _setup_branch(store_root, commits=[old, new])
640 ev = build_evaluator("author == alice")
641 results = walk_history(
642 store_root, "main", ev, load_manifest=False,
643 until=_dt(2025, 12, 31),
644 )
645 assert len(results) == 1
646 assert results[0]["committed_at"].startswith("2025")
647
648 def test_since_and_until_window(self, store_root: pathlib.Path) -> None:
649 dates = [_dt(2025, m, 1) for m in range(1, 13)]
650 prev: str | None = None
651 commits: list[CommitRecord] = []
652 for i, d in enumerate(dates):
653 c = _make_commit(store_root, author="alice", committed_at=d, parent=prev, message=f"month {i}")
654 commits.append(c)
655 prev = c.commit_id
656 _setup_branch(store_root, commits=commits)
657 ev = build_evaluator("author == alice")
658 results = walk_history(
659 store_root, "main", ev, load_manifest=False,
660 since=_dt(2025, 4, 1),
661 until=_dt(2025, 9, 1),
662 )
663 # April (4), May (5), Jun (6), Jul (7), Aug (8), Sep (9) = 6
664 assert len(results) == 6
665
666 def test_results_ordered_newest_first(self, store_root: pathlib.Path) -> None:
667 """walk_history traverses parent chain newest-first."""
668 prev: str | None = None
669 commits: list[CommitRecord] = []
670 for i in range(5):
671 c = _make_commit(
672 store_root, author="alice",
673 committed_at=_dt(2026, 1, i + 1),
674 parent=prev, message=f"order {i}",
675 )
676 commits.append(c)
677 prev = c.commit_id
678 _setup_branch(store_root, commits=commits)
679 ev = build_evaluator("author == alice")
680 results = walk_history(store_root, "main", ev, load_manifest=False)
681 timestamps = [r["committed_at"] for r in results]
682 assert timestamps == sorted(timestamps, reverse=True)
683
684 def test_head_commit_id_override(self, store_root: pathlib.Path) -> None:
685 c1 = _make_commit(store_root, author="alice", message="override alice")
686 c2 = _make_commit(store_root, author="bob", parent=c1.commit_id, message="override bob")
687 _setup_branch(store_root, commits=[c1, c2])
688 ev = build_evaluator("author == alice")
689 # Start from c1 directly, skipping c2.
690 results = walk_history(
691 store_root, "main", ev,
692 head_commit_id=c1.commit_id, load_manifest=False,
693 )
694 assert len(results) == 1
695
696 def test_broken_parent_chain_stops_gracefully(
697 self, store_root: pathlib.Path
698 ) -> None:
699 from muse.core.store import commit_path
700 # Satisfy the parent-existence guard by writing a stub file at the
701 # fake parent's path. walk_history will find it, fail to parse it as
702 # a valid commit (returns None), and stop — exactly what we're testing.
703 stub = commit_path(store_root, NULL_LONG_ID)
704 stub.parent.mkdir(parents=True, exist_ok=True)
705 stub.write_bytes(b"") # unreadable stub — read_commit returns None
706 c = _make_commit(
707 store_root, author="alice",
708 parent=NULL_COMMIT_ID, # points to the stub above
709 message="orphan commit",
710 )
711 _setup_branch(store_root, commits=[c])
712 ev = build_evaluator("author == alice")
713 results = walk_history(store_root, "main", ev, load_manifest=False)
714 # Reads c, then tries parent "0"*64 which is unreadable → stops.
715 assert len(results) == 1
716
717 def test_evaluator_exception_is_swallowed(
718 self, store_root: pathlib.Path
719 ) -> None:
720 """An evaluator that raises should not abort the walk — just skip that commit."""
721 c1 = _make_commit(store_root, author="alice", message="exception c1")
722 c2 = _make_commit(store_root, author="alice", parent=c1.commit_id, message="exception c2")
723 _setup_branch(store_root, commits=[c1, c2])
724
725 call_count = [0]
726
727 def flaky_ev(
728 commit: CommitRecord, manifest: Manifest, root: pathlib.Path
729 ) -> list[QueryMatch]:
730 call_count[0] += 1
731 if call_count[0] == 1:
732 raise RuntimeError("simulated evaluator failure")
733 return [
734 QueryMatch(
735 commit_id=commit.commit_id,
736 author=commit.author,
737 committed_at=commit.committed_at.isoformat(),
738 branch=commit.branch,
739 detail="ok",
740 extra={},
741 )
742 ]
743
744 results = walk_history(store_root, "main", flaky_ev, load_manifest=False)
745 assert len(results) == 1
746
747
748 # ---------------------------------------------------------------------------
749 # C — CLI E2E tests
750 # ---------------------------------------------------------------------------
751
752
753 def _seed_commit(
754 root: pathlib.Path,
755 branch: str = "main",
756 parent: str | None = None,
757 delta: StructuredDelta | None = None,
758 author: str = "alice",
759 agent_id: str = "",
760 sem_ver_bump: SemVerBump = "none",
761 committed_at: datetime.datetime | None = None,
762 message: str = "test commit",
763 ) -> CommitRecord:
764 """Write a commit with a content-addressed ID and advance the branch HEAD."""
765 c = _make_commit(
766 root, branch=branch, parent=parent, delta=delta,
767 author=author, agent_id=agent_id, sem_ver_bump=sem_ver_bump,
768 committed_at=committed_at, message=message,
769 )
770 refs_dir = heads_dir(root)
771 refs_dir.mkdir(parents=True, exist_ok=True)
772 (refs_dir / branch).write_text(c.commit_id)
773 return c
774
775
776 class TestCLI:
777 """E2E CLI tests using a real-init repo with crafted commit records."""
778
779 def test_count_flag(self, repo: pathlib.Path) -> None:
780 delta = _insert_delta("func_a", "func_b")
781 _seed_commit(repo, delta=delta, message="cli count")
782 code, out = _run(repo, "code", "code-query", "change == added", "--count")
783 assert code == 0
784 assert out.strip().isdigit()
785 assert int(out.strip()) >= 1
786
787 def test_count_no_matches(self, repo: pathlib.Path) -> None:
788 _seed_commit(repo, delta=None, message="cli count zero")
789 code, out = _run(repo, "code", "code-query", "author == nobody", "--count")
790 assert code == 0
791 assert out.strip() == "0"
792
793 def test_json_flag_returns_list(self, repo: pathlib.Path) -> None:
794 _seed_commit(repo, author="alice", message="cli json")
795 code, out = _run(repo, "code", "code-query", "author == alice", "--json")
796 assert code == 0
797 parsed = json.loads(out)
798 assert isinstance(parsed, dict)
799 assert "total" in parsed
800 assert isinstance(parsed["results"], list)
801
802 def test_json_match_has_required_keys(self, repo: pathlib.Path) -> None:
803 _seed_commit(repo, author="alice", message="cli-json-keys")
804 _, out = _run(repo, "code", "code-query", "author == alice", "--json")
805 parsed = json.loads(out)
806 matches = parsed["results"]
807 assert len(matches) >= 1
808 m = matches[0]
809 for key in ("commit_id", "author", "committed_at", "branch", "detail"):
810 assert key in m, f"missing key: {key}"
811
812 def test_json_no_matches_returns_empty_list(self, repo: pathlib.Path) -> None:
813 _seed_commit(repo, author="alice", message="cli-json-empty")
814 _, out = _run(repo, "code", "code-query", "author == nobody", "--json")
815 parsed = json.loads(out)
816 assert parsed["total"] == 0
817 assert parsed["results"] == []
818
819 def test_limit_caps_display(self, repo: pathlib.Path) -> None:
820 delta = _insert_delta(*[f"func_{i}" for i in range(30)])
821 _seed_commit(repo, delta=delta, message="cli-limit")
822 code, out = _run(
823 repo, "code", "code-query", "change == added", "--limit", "3"
824 )
825 assert code == 0
826 # "Found N match(es):" line + 3 detail lines + maybe truncation line
827 result_lines = [l for l in out.splitlines() if l.strip().startswith("src/")]
828 assert len(result_lines) <= 3
829
830 def test_endswith_operator_in_query(self, repo: pathlib.Path) -> None:
831 delta = _insert_delta("auth_handler", "data_service", file="src/routes.py")
832 _seed_commit(repo, delta=delta, message="cli-endswith")
833 _, out = _run(
834 repo, "code", "code-query", "symbol endswith _handler"
835 )
836 assert "handler" in out.lower() or "match" in out.lower()
837
838 def test_invalid_query_exits_1(self, repo: pathlib.Path) -> None:
839 code, _ = _run_unchecked(
840 repo, "code", "code-query", "nonexistent == value"
841 )
842 assert code == 1
843
844 def test_since_filters_correctly(self, repo: pathlib.Path) -> None:
845 old = _seed_commit(repo, author="alice", committed_at=_dt(2025, 1, 1), message="cli-since-old")
846 _seed_commit(
847 repo, author="alice",
848 committed_at=_dt(2026, 3, 1), parent=old.commit_id, message="cli-since-new",
849 )
850 _, out = _run(
851 repo, "code", "code-query", "author == alice",
852 "--since", "2026-01-01",
853 )
854 # Output should mention exactly 1 match (the 2026 commit).
855 assert "1 match" in out
856
857 def test_until_filters_correctly(self, repo: pathlib.Path) -> None:
858 old = _seed_commit(repo, author="alice", committed_at=_dt(2025, 1, 1), message="cli-until-old")
859 _seed_commit(
860 repo, author="alice",
861 committed_at=_dt(2026, 3, 26), parent=old.commit_id, message="cli-until-new",
862 )
863 _, out = _run(
864 repo, "code", "code-query", "author == alice",
865 "--until", "2025-12-31",
866 )
867 assert "1 match" in out
868
869 def test_invalid_since_date_exits_1(self, repo: pathlib.Path) -> None:
870 code, _ = _run_unchecked(
871 repo, "code", "code-query", "author == alice",
872 "--since", "not-a-date",
873 )
874 assert code == 1
875
876 def test_invalid_until_date_exits_1(self, repo: pathlib.Path) -> None:
877 code, _ = _run_unchecked(
878 repo, "code", "code-query", "author == alice",
879 "--until", "2026/01/01",
880 )
881 assert code == 1
882
883 def test_no_commits_on_branch_shows_no_matches(
884 self, repo: pathlib.Path
885 ) -> None:
886 code, out = _run(
887 repo, "code", "code-query", "author == alice",
888 "--branch", "nonexistent-branch",
889 )
890 assert code == 0
891 assert "No matches found" in out
892
893 def test_sem_ver_bump_query(self, repo: pathlib.Path) -> None:
894 _seed_commit(repo, author="alice", sem_ver_bump="major", message="cli-semver")
895 _, out = _run(repo, "code", "code-query", "sem_ver_bump == major")
896 assert "match" in out
897
898 def test_text_output_format_header(self, repo: pathlib.Path) -> None:
899 _seed_commit(repo, author="alice", message="cli-fmt")
900 _, out = _run(repo, "code", "code-query", "author == alice")
901 assert "Found" in out and "match" in out
902
903 def test_since_datetime_format_accepted(self, repo: pathlib.Path) -> None:
904 _seed_commit(repo, author="alice", committed_at=_dt(2026, 3, 26), message="cli-dt-fmt")
905 code, _ = _run(
906 repo, "code", "code-query", "author == alice",
907 "--since", "2026-03-01T00:00:00",
908 )
909 assert code == 0
910
911 def test_count_and_json_both_respected(self, repo: pathlib.Path) -> None:
912 """--count takes precedence over --json (count is printed as a number)."""
913 _seed_commit(repo, author="alice", message="cli-count-json")
914 code, out = _run(
915 repo, "code", "code-query", "author == alice", "--count", "--json"
916 )
917 assert code == 0
918 # --count wins; output should be a plain integer
919 assert out.strip().isdigit()
920
921
922 # ---------------------------------------------------------------------------
923 # S — Stress tests
924 # ---------------------------------------------------------------------------
925
926
927 class TestStress:
928 """High-volume and performance stress tests."""
929
930 def test_300_commits_all_match(self, store_root: pathlib.Path) -> None:
931 prev: str | None = None
932 commits: list[CommitRecord] = []
933 for i in range(300):
934 c = _make_commit(store_root, author="alice", parent=prev, message=f"stress {i}")
935 commits.append(c)
936 prev = c.commit_id
937 _setup_branch(store_root, commits=commits)
938 ev = build_evaluator("author == alice")
939 results = walk_history(
940 store_root, "main", ev, max_commits=300, load_manifest=False
941 )
942 assert len(results) == 300
943
944 def test_300_commits_none_match(self, store_root: pathlib.Path) -> None:
945 prev: str | None = None
946 commits: list[CommitRecord] = []
947 for i in range(300):
948 c = _make_commit(store_root, author="alice", parent=prev, message=f"miss {i}")
949 commits.append(c)
950 prev = c.commit_id
951 _setup_branch(store_root, commits=commits)
952 ev = build_evaluator("author == bob")
953 results = walk_history(
954 store_root, "main", ev, max_commits=300, load_manifest=False
955 )
956 assert results == []
957
958 def test_large_or_expression_evaluator(self) -> None:
959 """50-clause OR expression; evaluator must not degrade."""
960 clauses = " or ".join(f"author == 'agent_{i}'" for i in range(50))
961 ev = build_evaluator(clauses)
962 commit = _bare_commit(author="agent_49")
963 results = ev(commit, {}, pathlib.Path("."))
964 assert len(results) >= 1
965
966 def test_50_symbols_per_commit_cap_is_enforced(
967 self, store_root: pathlib.Path
968 ) -> None:
969 """200 matching symbols in one commit must produce exactly 20 results (cap)."""
970 symbols = [f"func_{i}" for i in range(200)]
971 delta = _insert_delta(*symbols)
972 c = _make_commit(store_root, delta=delta, message="stress cap commit")
973 _setup_branch(store_root, commits=[c])
974 ev = build_evaluator("change == added")
975 results = walk_history(
976 store_root, "main", ev, load_manifest=False
977 )
978 assert len(results) == 20
979
980 def test_load_manifest_false_never_reads_manifest_in_300_commit_walk(
981 self, store_root: pathlib.Path
982 ) -> None:
983 """Critical: manifest I/O must be zero when load_manifest=False."""
984 prev: str | None = None
985 commits: list[CommitRecord] = []
986 for i in range(300):
987 c = _make_commit(store_root, author="alice", parent=prev, message=f"nomani {i}")
988 commits.append(c)
989 prev = c.commit_id
990 _setup_branch(store_root, commits=commits)
991 ev = build_evaluator("author == alice")
992 with patch(
993 "muse.core.query_engine.get_commit_snapshot_manifest"
994 ) as mock_m:
995 walk_history(
996 store_root, "main", ev, max_commits=300, load_manifest=False
997 )
998 mock_m.assert_not_called()
999
1000 def test_mixed_delta_and_no_delta_commits(
1001 self, store_root: pathlib.Path
1002 ) -> None:
1003 """Commits with and without deltas co-exist; walk must not crash."""
1004 prev: str | None = None
1005 commits: list[CommitRecord] = []
1006 for i in range(50):
1007 delta = _insert_delta("func") if i % 2 == 0 else None
1008 c = _make_commit(store_root, author="alice", delta=delta, parent=prev, message=f"mixed {i}")
1009 commits.append(c)
1010 prev = c.commit_id
1011 _setup_branch(store_root, commits=commits)
1012 ev = build_evaluator("author == alice")
1013 results = walk_history(store_root, "main", ev, max_commits=50, load_manifest=False)
1014 assert len(results) == 50
1015
1016
1017 # ---------------------------------------------------------------------------
1018 # R — Regression tests (named for specific bugs fixed)
1019 # ---------------------------------------------------------------------------
1020
1021
1022 class TestRegressions:
1023 """One test per bug fixed — guaranteed not to regress."""
1024
1025 def test_walk_history_uses_store_not_direct_ref_read(
1026 self, store_root: pathlib.Path
1027 ) -> None:
1028 """walk_history must call get_head_commit_id, not read the ref file directly."""
1029 c = _make_commit(store_root, author="alice", message="reg store commit")
1030 _setup_branch(store_root, commits=[c])
1031 ev = build_evaluator("author == alice")
1032 with patch(
1033 "muse.core.query_engine.get_head_commit_id",
1034 wraps=__import__(
1035 "muse.core.store", fromlist=["get_head_commit_id"]
1036 ).get_head_commit_id,
1037 ) as mock_fn:
1038 walk_history(store_root, "main", ev, load_manifest=False)
1039 mock_fn.assert_called_once_with(store_root, "main")
1040
1041 def test_endswith_operator_not_silently_ignored(self) -> None:
1042 """Regression: endswith was missing from CodeOp, causing ValueError."""
1043 # This would have raised ValueError: "Unknown operator: 'endswith'" before the fix.
1044 ev = build_evaluator("symbol endswith _service")
1045 delta = _insert_delta("auth_service")
1046 commit = _bare_commit(delta=delta)
1047 results = ev(commit, {}, pathlib.Path("."))
1048 assert len(results) >= 1
1049
1050 def test_dead_field_val_none_check_removed(self) -> None:
1051 """field_val from .get(f, '') is always str — 'is not None' was dead code.
1052
1053 Verify field matching still works correctly after the dead-check removal.
1054 """
1055 delta = _insert_delta("my_func", file="src/core.py")
1056 ev = build_evaluator("file == 'src/core.py'")
1057 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
1058 assert len(results) >= 1
1059
1060 def test_patch_op_redundant_condition_fixed(self) -> None:
1061 """Regression: 'op_rec.get("op") == "patch" and op_rec["op"] == "patch"'
1062 was redundant and now replaced by _is_patch_op TypeGuard.
1063 PatchOp child_ops must still be traversed correctly.
1064 """
1065 child: InsertOp = InsertOp(
1066 op="insert",
1067 address="lib/utils.py::parse",
1068 position=None,
1069 content_id="a" * 64,
1070 content_summary="parse added",
1071 )
1072 patch_op: PatchOp = PatchOp(op="patch", address="lib/utils.py", child_ops=[child], child_domain="code", child_summary="")
1073 delta: StructuredDelta = StructuredDelta(
1074 domain="code", ops=[patch_op], summary="patched utils"
1075 )
1076 ev = build_evaluator("symbol == parse")
1077 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
1078 assert len(results) >= 1
1079
1080 def test_json_output_is_list_not_wrapped_list(self, repo: pathlib.Path) -> None:
1081 """JSON output is {total, results} — results is a flat list of match dicts."""
1082 _seed_commit(repo, author="alice", message="reg-json-list")
1083 _, out = _run(repo, "code", "code-query", "author == alice", "--json")
1084 parsed = json.loads(out)
1085 assert isinstance(parsed, dict)
1086 assert "total" in parsed
1087 assert isinstance(parsed["results"], list)
1088 assert parsed["total"] == len(parsed["results"])
1089 if parsed["results"]:
1090 assert isinstance(parsed["results"][0], dict)
1091
1092 def test_mixed_or_commit_level_clause_was_silently_dropped(
1093 self, store_root: pathlib.Path
1094 ) -> None:
1095 """Regression: with the old double-pass, a commit matching the FIRST
1096 OR clause (commit-level) would produce symbol_matches=[] and then fail
1097 the 'only_commit_fields' check if the SECOND clause used a symbol field —
1098 resulting in a silent drop. The new or_matched flag fixes this.
1099 """
1100 c = _make_commit(
1101 store_root, author="alice", delta=None, message="reg or drop" # no delta at all
1102 )
1103 _setup_branch(store_root, commits=[c])
1104 # Mixed OR: first clause is commit-level (matches), second is symbol-level.
1105 ev = build_evaluator("author == 'alice' or change == 'added'")
1106 results = walk_history(store_root, "main", ev, load_manifest=False)
1107 # alice's commit must appear even though change=='added' can't match (no delta).
1108 assert len(results) == 1
1109
1110
1111 # ---------------------------------------------------------------------------
1112 # TestRegisterFlags
1113 # ---------------------------------------------------------------------------
1114
1115
1116 class TestRegisterFlags:
1117 """register() wires --json / -j correctly."""
1118
1119 def _parse(self, *args: str) -> argparse.Namespace:
1120 from muse.cli.commands.code_query import register
1121 p = argparse.ArgumentParser()
1122 sub = p.add_subparsers()
1123 register(sub)
1124 return p.parse_args(["code-query", *args])
1125
1126 def test_default_json_out_is_false(self) -> None:
1127 ns = self._parse("author == 'x'")
1128 assert ns.json_out is False
1129
1130 def test_json_flag_sets_json_out(self) -> None:
1131 ns = self._parse("--json", "author == 'x'")
1132 assert ns.json_out is True
1133
1134 def test_j_shorthand_sets_json_out(self) -> None:
1135 ns = self._parse("-j", "author == 'x'")
1136 assert ns.json_out is True
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 121 days ago