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