gabriel / muse public
test_cmd_code_query.py python
1,099 lines 43.8 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 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 datetime
37 import hashlib
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
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 """Deterministic 64-char hex commit ID derived from *parts*."""
89 return hashlib.sha256("|".join(parts).encode()).hexdigest()
90
91
92 def _now() -> datetime.datetime:
93 return datetime.datetime.now(datetime.timezone.utc)
94
95
96 def _dt(year: int = 2026, month: int = 3, day: int = 1) -> datetime.datetime:
97 return datetime.datetime(year, month, day, tzinfo=datetime.timezone.utc)
98
99
100 def _insert_delta(*symbols: str, file: str = "src/foo.py") -> StructuredDelta:
101 ops: list[DomainOp] = [
102 InsertOp(
103 op="insert",
104 address=f"{file}::{sym}",
105 position=None,
106 content_id=_fake_id(sym),
107 content_summary=f"added {sym}",
108 )
109 for sym in symbols
110 ]
111 return StructuredDelta(domain="code", ops=ops, summary=f"{len(ops)} symbol(s) added")
112
113
114 def _delete_delta(symbol: str, file: str = "src/foo.py") -> StructuredDelta:
115 op = DeleteOp(
116 op="delete",
117 address=f"{file}::{symbol}",
118 content_id=_fake_id(symbol),
119 position=None,
120 content_summary=f"deleted {symbol}",
121 )
122 return StructuredDelta(domain="code", ops=[op], summary="1 symbol deleted")
123
124
125 def _make_commit(
126 root: pathlib.Path,
127 branch: str = "main",
128 parent: str | None = None,
129 delta: StructuredDelta | None = None,
130 author: str = "alice",
131 agent_id: str = "",
132 model_id: str = "",
133 sem_ver_bump: SemVerBump = "none",
134 committed_at: datetime.datetime | None = None,
135 message: str = "test commit",
136 ) -> CommitRecord:
137 """Write a CommitRecord with a content-addressed ID to *root* and return it."""
138 snap_id = compute_snapshot_id({})
139 committed_at_val = committed_at or _now()
140 parent_ids = [parent] if parent else []
141 commit_id = compute_commit_id(parent_ids, snap_id, message, committed_at_val.isoformat())
142 rec = CommitRecord(
143 commit_id=commit_id,
144 repo_id="test-repo",
145 branch=branch,
146 snapshot_id=snap_id,
147 message=message,
148 committed_at=committed_at_val,
149 parent_commit_id=parent,
150 author=author,
151 agent_id=agent_id,
152 model_id=model_id,
153 sem_ver_bump=sem_ver_bump,
154 structured_delta=delta,
155 )
156 write_commit(root, rec)
157 return rec
158
159
160 def _setup_branch(
161 root: pathlib.Path,
162 branch: str = "main",
163 commits: list[CommitRecord] | None = None,
164 ) -> None:
165 """Wire up HEAD and branch ref so walk_history can find the commits."""
166 (root / ".muse").mkdir(exist_ok=True)
167 (root / ".muse" / "HEAD").write_text(branch)
168 refs_dir = root / ".muse" / "refs" / "heads"
169 refs_dir.mkdir(parents=True, exist_ok=True)
170 if commits:
171 (refs_dir / branch).write_text(commits[-1].commit_id)
172
173
174 @pytest.fixture()
175 def store_root(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
176 """Minimal repo layout: .muse/ directories, no branch yet."""
177 (tmp_path / ".muse" / "commits").mkdir(parents=True)
178 (tmp_path / ".muse" / "refs" / "heads").mkdir(parents=True)
179 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
180 return tmp_path
181
182
183 @pytest.fixture()
184 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
185 """Full muse-init repo for E2E CLI tests."""
186 monkeypatch.chdir(tmp_path)
187 r = runner.invoke(cli, ["init", "--domain", "code"], env=_env(tmp_path))
188 assert r.exit_code == 0, r.output
189 return tmp_path
190
191
192 # ---------------------------------------------------------------------------
193 # P — Parser tests
194 # ---------------------------------------------------------------------------
195
196
197 class TestParser:
198 """Tokenizer and parser unit tests."""
199
200 def test_endswith_operator_parsed(self) -> None:
201 q = _parse_query("symbol endswith _handler")
202 cmp = q.clauses[0].clauses[0]
203 assert cmp.op == "endswith"
204 assert cmp.value == "_handler"
205
206 def test_all_operators_accepted(self) -> None:
207 for op in ("==", "!=", "contains", "startswith", "endswith"):
208 q = _parse_query(f"author {op} alice")
209 assert q.clauses[0].clauses[0].op == op
210
211 def test_all_valid_fields_accepted(self) -> None:
212 fields = [
213 "symbol", "file", "language", "kind", "change",
214 "author", "agent_id", "model_id", "toolchain_id",
215 "sem_ver_bump", "branch",
216 ]
217 for f in fields:
218 q = _parse_query(f"{f} == test")
219 assert q.clauses[0].clauses[0].field == f
220
221 def test_complex_and_or_query(self) -> None:
222 q = _parse_query("author == 'alice' and change == 'added' or author == 'bob'")
223 assert isinstance(q, OrExpr)
224 assert len(q.clauses) == 2
225 assert len(q.clauses[0].clauses) == 2
226 assert len(q.clauses[1].clauses) == 1
227
228 def test_single_quoted_value(self) -> None:
229 q = _parse_query("agent_id == 'claude-4'")
230 assert q.clauses[0].clauses[0].value == "claude-4"
231
232 def test_double_quoted_value(self) -> None:
233 q = _parse_query('model_id == "claude-opus-4"')
234 assert q.clauses[0].clauses[0].value == "claude-opus-4"
235
236 def test_unquoted_word_value(self) -> None:
237 q = _parse_query("branch == dev")
238 assert q.clauses[0].clauses[0].value == "dev"
239
240 def test_unknown_field_raises(self) -> None:
241 with pytest.raises(ValueError, match="Unknown field"):
242 _parse_query("nonexistent == 'x'")
243
244 def test_unknown_operator_raises(self) -> None:
245 with pytest.raises(ValueError, match="Unknown operator"):
246 _parse_query("author like alice")
247
248 def test_multiple_and_clauses(self) -> None:
249 q = _parse_query("author == 'alice' and change == 'added' and kind == 'function'")
250 assert len(q.clauses[0].clauses) == 3
251
252 def test_multiple_or_clauses(self) -> None:
253 q = _parse_query("author == 'a' or author == 'b' or author == 'c'")
254 assert len(q.clauses) == 3
255
256 def test_endswith_in_and_chain(self) -> None:
257 q = _parse_query("file endswith .py and symbol endswith _test")
258 clauses = q.clauses[0].clauses
259 assert clauses[0].op == "endswith"
260 assert clauses[1].op == "endswith"
261
262 def test_sem_ver_bump_values_accepted(self) -> None:
263 for val in ("none", "patch", "minor", "major"):
264 q = _parse_query(f"sem_ver_bump == {val}")
265 assert q.clauses[0].clauses[0].value == val
266
267
268 # ---------------------------------------------------------------------------
269 # E — Evaluator unit tests
270 # ---------------------------------------------------------------------------
271
272
273 def _bare_commit(
274 author: str = "alice",
275 agent_id: str = "",
276 model_id: str = "",
277 branch: str = "main",
278 sem_ver_bump: SemVerBump = "none",
279 delta: StructuredDelta | None = None,
280 message: str = "test",
281 ) -> CommitRecord:
282 return CommitRecord(
283 commit_id=_fake_id(author, agent_id, branch),
284 repo_id="r",
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 c = _make_commit(
698 store_root, author="alice",
699 parent="0" * 64, # non-existent parent
700 message="orphan commit",
701 )
702 _setup_branch(store_root, commits=[c])
703 ev = build_evaluator("author == alice")
704 results = walk_history(store_root, "main", ev, load_manifest=False)
705 # Reads c, then tries parent "0"*64 which doesn't exist → stops.
706 assert len(results) == 1
707
708 def test_evaluator_exception_is_swallowed(
709 self, store_root: pathlib.Path
710 ) -> None:
711 """An evaluator that raises should not abort the walk — just skip that commit."""
712 c1 = _make_commit(store_root, author="alice", message="exception c1")
713 c2 = _make_commit(store_root, author="alice", parent=c1.commit_id, message="exception c2")
714 _setup_branch(store_root, commits=[c1, c2])
715
716 call_count = [0]
717
718 def flaky_ev(
719 commit: CommitRecord, manifest: Manifest, root: pathlib.Path
720 ) -> list[QueryMatch]:
721 call_count[0] += 1
722 if call_count[0] == 1:
723 raise RuntimeError("simulated evaluator failure")
724 return [
725 QueryMatch(
726 commit_id=commit.commit_id,
727 author=commit.author,
728 committed_at=commit.committed_at.isoformat(),
729 branch=commit.branch,
730 detail="ok",
731 extra={},
732 )
733 ]
734
735 results = walk_history(store_root, "main", flaky_ev, load_manifest=False)
736 assert len(results) == 1
737
738
739 # ---------------------------------------------------------------------------
740 # C — CLI E2E tests
741 # ---------------------------------------------------------------------------
742
743
744 def _seed_commit(
745 root: pathlib.Path,
746 branch: str = "main",
747 parent: str | None = None,
748 delta: StructuredDelta | None = None,
749 author: str = "alice",
750 agent_id: str = "",
751 sem_ver_bump: SemVerBump = "none",
752 committed_at: datetime.datetime | None = None,
753 message: str = "test commit",
754 ) -> CommitRecord:
755 """Write a commit with a content-addressed ID and advance the branch HEAD."""
756 c = _make_commit(
757 root, branch=branch, parent=parent, delta=delta,
758 author=author, agent_id=agent_id, sem_ver_bump=sem_ver_bump,
759 committed_at=committed_at, message=message,
760 )
761 refs_dir = root / ".muse" / "refs" / "heads"
762 refs_dir.mkdir(parents=True, exist_ok=True)
763 (refs_dir / branch).write_text(c.commit_id)
764 return c
765
766
767 class TestCLI:
768 """E2E CLI tests using a real-init repo with crafted commit records."""
769
770 def test_count_flag(self, repo: pathlib.Path) -> None:
771 delta = _insert_delta("func_a", "func_b")
772 _seed_commit(repo, delta=delta, message="cli count")
773 code, out = _run(repo, "code", "code-query", "change == added", "--count")
774 assert code == 0
775 assert out.strip().isdigit()
776 assert int(out.strip()) >= 1
777
778 def test_count_no_matches(self, repo: pathlib.Path) -> None:
779 _seed_commit(repo, delta=None, message="cli count zero")
780 code, out = _run(repo, "code", "code-query", "author == nobody", "--count")
781 assert code == 0
782 assert out.strip() == "0"
783
784 def test_json_flag_returns_list(self, repo: pathlib.Path) -> None:
785 _seed_commit(repo, author="alice", message="cli json")
786 code, out = _run(repo, "code", "code-query", "author == alice", "--json")
787 assert code == 0
788 parsed = json.loads(out)
789 assert isinstance(parsed, dict)
790 assert "total" in parsed
791 assert isinstance(parsed["results"], list)
792
793 def test_json_match_has_required_keys(self, repo: pathlib.Path) -> None:
794 _seed_commit(repo, author="alice", message="cli-json-keys")
795 _, out = _run(repo, "code", "code-query", "author == alice", "--json")
796 parsed = json.loads(out)
797 matches = parsed["results"]
798 assert len(matches) >= 1
799 m = matches[0]
800 for key in ("commit_id", "author", "committed_at", "branch", "detail"):
801 assert key in m, f"missing key: {key}"
802
803 def test_json_no_matches_returns_empty_list(self, repo: pathlib.Path) -> None:
804 _seed_commit(repo, author="alice", message="cli-json-empty")
805 _, out = _run(repo, "code", "code-query", "author == nobody", "--json")
806 parsed = json.loads(out)
807 assert parsed["total"] == 0
808 assert parsed["results"] == []
809
810 def test_limit_caps_display(self, repo: pathlib.Path) -> None:
811 delta = _insert_delta(*[f"func_{i}" for i in range(30)])
812 _seed_commit(repo, delta=delta, message="cli-limit")
813 code, out = _run(
814 repo, "code", "code-query", "change == added", "--limit", "3"
815 )
816 assert code == 0
817 # "Found N match(es):" line + 3 detail lines + maybe truncation line
818 result_lines = [l for l in out.splitlines() if l.strip().startswith("src/")]
819 assert len(result_lines) <= 3
820
821 def test_endswith_operator_in_query(self, repo: pathlib.Path) -> None:
822 delta = _insert_delta("auth_handler", "data_service", file="src/routes.py")
823 _seed_commit(repo, delta=delta, message="cli-endswith")
824 _, out = _run(
825 repo, "code", "code-query", "symbol endswith _handler"
826 )
827 assert "handler" in out.lower() or "match" in out.lower()
828
829 def test_invalid_query_exits_1(self, repo: pathlib.Path) -> None:
830 code, _ = _run_unchecked(
831 repo, "code", "code-query", "nonexistent == value"
832 )
833 assert code == 1
834
835 def test_since_filters_correctly(self, repo: pathlib.Path) -> None:
836 old = _seed_commit(repo, author="alice", committed_at=_dt(2025, 1, 1), message="cli-since-old")
837 _seed_commit(
838 repo, author="alice",
839 committed_at=_dt(2026, 3, 1), parent=old.commit_id, message="cli-since-new",
840 )
841 _, out = _run(
842 repo, "code", "code-query", "author == alice",
843 "--since", "2026-01-01",
844 )
845 # Output should mention exactly 1 match (the 2026 commit).
846 assert "1 match" in out
847
848 def test_until_filters_correctly(self, repo: pathlib.Path) -> None:
849 old = _seed_commit(repo, author="alice", committed_at=_dt(2025, 1, 1), message="cli-until-old")
850 _seed_commit(
851 repo, author="alice",
852 committed_at=_dt(2026, 3, 26), parent=old.commit_id, message="cli-until-new",
853 )
854 _, out = _run(
855 repo, "code", "code-query", "author == alice",
856 "--until", "2025-12-31",
857 )
858 assert "1 match" in out
859
860 def test_invalid_since_date_exits_1(self, repo: pathlib.Path) -> None:
861 code, _ = _run_unchecked(
862 repo, "code", "code-query", "author == alice",
863 "--since", "not-a-date",
864 )
865 assert code == 1
866
867 def test_invalid_until_date_exits_1(self, repo: pathlib.Path) -> None:
868 code, _ = _run_unchecked(
869 repo, "code", "code-query", "author == alice",
870 "--until", "2026/01/01",
871 )
872 assert code == 1
873
874 def test_no_commits_on_branch_shows_no_matches(
875 self, repo: pathlib.Path
876 ) -> None:
877 code, out = _run(
878 repo, "code", "code-query", "author == alice",
879 "--branch", "nonexistent-branch",
880 )
881 assert code == 0
882 assert "No matches found" in out
883
884 def test_sem_ver_bump_query(self, repo: pathlib.Path) -> None:
885 _seed_commit(repo, author="alice", sem_ver_bump="major", message="cli-semver")
886 _, out = _run(repo, "code", "code-query", "sem_ver_bump == major")
887 assert "match" in out
888
889 def test_text_output_format_header(self, repo: pathlib.Path) -> None:
890 _seed_commit(repo, author="alice", message="cli-fmt")
891 _, out = _run(repo, "code", "code-query", "author == alice")
892 assert "Found" in out and "match" in out
893
894 def test_since_datetime_format_accepted(self, repo: pathlib.Path) -> None:
895 _seed_commit(repo, author="alice", committed_at=_dt(2026, 3, 26), message="cli-dt-fmt")
896 code, _ = _run(
897 repo, "code", "code-query", "author == alice",
898 "--since", "2026-03-01T00:00:00",
899 )
900 assert code == 0
901
902 def test_count_and_json_both_respected(self, repo: pathlib.Path) -> None:
903 """--count takes precedence over --json (count is printed as a number)."""
904 _seed_commit(repo, author="alice", message="cli-count-json")
905 code, out = _run(
906 repo, "code", "code-query", "author == alice", "--count", "--json"
907 )
908 assert code == 0
909 # --count wins; output should be a plain integer
910 assert out.strip().isdigit()
911
912
913 # ---------------------------------------------------------------------------
914 # S — Stress tests
915 # ---------------------------------------------------------------------------
916
917
918 class TestStress:
919 """High-volume and performance stress tests."""
920
921 def test_300_commits_all_match(self, store_root: pathlib.Path) -> None:
922 prev: str | None = None
923 commits: list[CommitRecord] = []
924 for i in range(300):
925 c = _make_commit(store_root, author="alice", parent=prev, message=f"stress {i}")
926 commits.append(c)
927 prev = c.commit_id
928 _setup_branch(store_root, commits=commits)
929 ev = build_evaluator("author == alice")
930 results = walk_history(
931 store_root, "main", ev, max_commits=300, load_manifest=False
932 )
933 assert len(results) == 300
934
935 def test_300_commits_none_match(self, store_root: pathlib.Path) -> None:
936 prev: str | None = None
937 commits: list[CommitRecord] = []
938 for i in range(300):
939 c = _make_commit(store_root, author="alice", parent=prev, message=f"miss {i}")
940 commits.append(c)
941 prev = c.commit_id
942 _setup_branch(store_root, commits=commits)
943 ev = build_evaluator("author == bob")
944 results = walk_history(
945 store_root, "main", ev, max_commits=300, load_manifest=False
946 )
947 assert results == []
948
949 def test_large_or_expression_evaluator(self) -> None:
950 """50-clause OR expression; evaluator must not degrade."""
951 clauses = " or ".join(f"author == 'agent_{i}'" for i in range(50))
952 ev = build_evaluator(clauses)
953 commit = _bare_commit(author="agent_49")
954 results = ev(commit, {}, pathlib.Path("."))
955 assert len(results) >= 1
956
957 def test_50_symbols_per_commit_cap_is_enforced(
958 self, store_root: pathlib.Path
959 ) -> None:
960 """200 matching symbols in one commit must produce exactly 20 results (cap)."""
961 symbols = [f"func_{i}" for i in range(200)]
962 delta = _insert_delta(*symbols)
963 c = _make_commit(store_root, delta=delta, message="stress cap commit")
964 _setup_branch(store_root, commits=[c])
965 ev = build_evaluator("change == added")
966 results = walk_history(
967 store_root, "main", ev, load_manifest=False
968 )
969 assert len(results) == 20
970
971 def test_load_manifest_false_never_reads_manifest_in_300_commit_walk(
972 self, store_root: pathlib.Path
973 ) -> None:
974 """Critical: manifest I/O must be zero when load_manifest=False."""
975 prev: str | None = None
976 commits: list[CommitRecord] = []
977 for i in range(300):
978 c = _make_commit(store_root, author="alice", parent=prev, message=f"nomani {i}")
979 commits.append(c)
980 prev = c.commit_id
981 _setup_branch(store_root, commits=commits)
982 ev = build_evaluator("author == alice")
983 with patch(
984 "muse.core.query_engine.get_commit_snapshot_manifest"
985 ) as mock_m:
986 walk_history(
987 store_root, "main", ev, max_commits=300, load_manifest=False
988 )
989 mock_m.assert_not_called()
990
991 def test_mixed_delta_and_no_delta_commits(
992 self, store_root: pathlib.Path
993 ) -> None:
994 """Commits with and without deltas co-exist; walk must not crash."""
995 prev: str | None = None
996 commits: list[CommitRecord] = []
997 for i in range(50):
998 delta = _insert_delta("func") if i % 2 == 0 else None
999 c = _make_commit(store_root, author="alice", delta=delta, parent=prev, message=f"mixed {i}")
1000 commits.append(c)
1001 prev = c.commit_id
1002 _setup_branch(store_root, commits=commits)
1003 ev = build_evaluator("author == alice")
1004 results = walk_history(store_root, "main", ev, max_commits=50, load_manifest=False)
1005 assert len(results) == 50
1006
1007
1008 # ---------------------------------------------------------------------------
1009 # R — Regression tests (named for specific bugs fixed)
1010 # ---------------------------------------------------------------------------
1011
1012
1013 class TestRegressions:
1014 """One test per bug fixed — guaranteed not to regress."""
1015
1016 def test_walk_history_uses_store_not_direct_ref_read(
1017 self, store_root: pathlib.Path
1018 ) -> None:
1019 """walk_history must call get_head_commit_id, not read the ref file directly."""
1020 c = _make_commit(store_root, author="alice", message="reg store commit")
1021 _setup_branch(store_root, commits=[c])
1022 ev = build_evaluator("author == alice")
1023 with patch(
1024 "muse.core.query_engine.get_head_commit_id",
1025 wraps=__import__(
1026 "muse.core.store", fromlist=["get_head_commit_id"]
1027 ).get_head_commit_id,
1028 ) as mock_fn:
1029 walk_history(store_root, "main", ev, load_manifest=False)
1030 mock_fn.assert_called_once_with(store_root, "main")
1031
1032 def test_endswith_operator_not_silently_ignored(self) -> None:
1033 """Regression: endswith was missing from CodeOp, causing ValueError."""
1034 # This would have raised ValueError: "Unknown operator: 'endswith'" before the fix.
1035 ev = build_evaluator("symbol endswith _service")
1036 delta = _insert_delta("auth_service")
1037 commit = _bare_commit(delta=delta)
1038 results = ev(commit, {}, pathlib.Path("."))
1039 assert len(results) >= 1
1040
1041 def test_dead_field_val_none_check_removed(self) -> None:
1042 """field_val from .get(f, '') is always str — 'is not None' was dead code.
1043
1044 Verify field matching still works correctly after the dead-check removal.
1045 """
1046 delta = _insert_delta("my_func", file="src/core.py")
1047 ev = build_evaluator("file == 'src/core.py'")
1048 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
1049 assert len(results) >= 1
1050
1051 def test_patch_op_redundant_condition_fixed(self) -> None:
1052 """Regression: 'op_rec.get("op") == "patch" and op_rec["op"] == "patch"'
1053 was redundant and now replaced by _is_patch_op TypeGuard.
1054 PatchOp child_ops must still be traversed correctly.
1055 """
1056 child: InsertOp = InsertOp(
1057 op="insert",
1058 address="lib/utils.py::parse",
1059 position=None,
1060 content_id="a" * 64,
1061 content_summary="parse added",
1062 )
1063 patch_op: PatchOp = PatchOp(op="patch", address="lib/utils.py", child_ops=[child], child_domain="code", child_summary="")
1064 delta: StructuredDelta = StructuredDelta(
1065 domain="code", ops=[patch_op], summary="patched utils"
1066 )
1067 ev = build_evaluator("symbol == parse")
1068 results = ev(_bare_commit(delta=delta), {}, pathlib.Path("."))
1069 assert len(results) >= 1
1070
1071 def test_json_output_is_list_not_wrapped_list(self, repo: pathlib.Path) -> None:
1072 """JSON output is {total, results} — results is a flat list of match dicts."""
1073 _seed_commit(repo, author="alice", message="reg-json-list")
1074 _, out = _run(repo, "code", "code-query", "author == alice", "--json")
1075 parsed = json.loads(out)
1076 assert isinstance(parsed, dict)
1077 assert "total" in parsed
1078 assert isinstance(parsed["results"], list)
1079 assert parsed["total"] == len(parsed["results"])
1080 if parsed["results"]:
1081 assert isinstance(parsed["results"][0], dict)
1082
1083 def test_mixed_or_commit_level_clause_was_silently_dropped(
1084 self, store_root: pathlib.Path
1085 ) -> None:
1086 """Regression: with the old double-pass, a commit matching the FIRST
1087 OR clause (commit-level) would produce symbol_matches=[] and then fail
1088 the 'only_commit_fields' check if the SECOND clause used a symbol field —
1089 resulting in a silent drop. The new or_matched flag fixes this.
1090 """
1091 c = _make_commit(
1092 store_root, author="alice", delta=None, message="reg or drop" # no delta at all
1093 )
1094 _setup_branch(store_root, commits=[c])
1095 # Mixed OR: first clause is commit-level (matches), second is symbol-level.
1096 ev = build_evaluator("author == 'alice' or change == 'added'")
1097 results = walk_history(store_root, "main", ev, load_manifest=False)
1098 # alice's commit must appear even though change=='added' can't match (no delta).
1099 assert len(results) == 1
File History 5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 48 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago