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