test_cmd_symlog_grep.py
python
sha256:ce542c4d0f6dacc9520c02d0e3378bf96bc377d118a7402f2bbc8af1749b08e0
feat(code-test): audit cache internals, add cache_hit/--cac…
Sonnet 5
minor
⚠ breaking
6 hours ago
| 1 | """muse#53 item 8 -- `muse symlog grep PATTERN [--file path] [--json]`. |
| 2 | |
| 3 | Searches symbol bodies across their full symlog history for PATTERN, |
| 4 | answering "when did this symbol last contain string X?" |
| 5 | """ |
| 6 | |
| 7 | from __future__ import annotations |
| 8 | |
| 9 | import json |
| 10 | import os |
| 11 | import pathlib |
| 12 | import textwrap |
| 13 | |
| 14 | from tests.cli_test_helper import CliRunner |
| 15 | |
| 16 | runner = CliRunner() |
| 17 | |
| 18 | |
| 19 | def _invoke(repo: pathlib.Path, args: list[str]): |
| 20 | saved = os.getcwd() |
| 21 | try: |
| 22 | os.chdir(repo) |
| 23 | return runner.invoke(None, args) |
| 24 | finally: |
| 25 | os.chdir(saved) |
| 26 | |
| 27 | |
| 28 | def _init_repo(repo: pathlib.Path) -> None: |
| 29 | repo.mkdir(parents=True, exist_ok=True) |
| 30 | _invoke(repo, ["init"]) |
| 31 | |
| 32 | |
| 33 | def _write_file(repo: pathlib.Path, rel: str, content: str) -> None: |
| 34 | p = repo / rel |
| 35 | p.parent.mkdir(parents=True, exist_ok=True) |
| 36 | p.write_text(textwrap.dedent(content)) |
| 37 | |
| 38 | |
| 39 | def _commit(repo: pathlib.Path, msg: str) -> str: |
| 40 | _invoke(repo, ["code", "add", "."]) |
| 41 | result = _invoke(repo, [ |
| 42 | "commit", "-m", msg, |
| 43 | "--agent-id", "claude-code", |
| 44 | "--model-id", "claude-sonnet-4-6", |
| 45 | "--author", "claude-code", |
| 46 | "--json", |
| 47 | ]) |
| 48 | data = json.loads(result.stdout) |
| 49 | return data["commit_id"] |
| 50 | |
| 51 | |
| 52 | def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: |
| 53 | _init_repo(tmp_path) |
| 54 | return tmp_path |
| 55 | |
| 56 | |
| 57 | def _symlog(repo: pathlib.Path, *args: str): |
| 58 | return _invoke(repo, ["symlog", *args]) |
| 59 | |
| 60 | |
| 61 | class TestSymlogGrep: |
| 62 | def test_finds_pattern_in_current_body(self, tmp_path: pathlib.Path) -> None: |
| 63 | repo = _make_repo(tmp_path) |
| 64 | _write_file(repo, "src/billing.py", "def compute_total(items):\n # deprecated: use v2\n return sum(items)\n") |
| 65 | _commit(repo, "add compute_total") |
| 66 | |
| 67 | result = _symlog(repo, "grep", "deprecated", "--json") |
| 68 | assert result.exit_code == 0, result.stdout |
| 69 | data = json.loads(result.stdout) |
| 70 | assert data["pattern"] == "deprecated" |
| 71 | assert data["count"] == 1 |
| 72 | assert data["matches"][0]["symbol"] == "src/billing.py::compute_total" |
| 73 | assert data["matches"][0]["index"] == 0 |
| 74 | |
| 75 | def test_finds_pattern_only_in_older_version(self, tmp_path: pathlib.Path) -> None: |
| 76 | repo = _make_repo(tmp_path) |
| 77 | # A comment-only diff doesn't change the symbol's AST content_id (no |
| 78 | # new symlog entry is written), so use a real statement to ensure two |
| 79 | # distinct entries exist. |
| 80 | _write_file(repo, "src/billing.py", "def compute_total(items):\n marker = 'deprecated_v1'\n return sum(items)\n") |
| 81 | _commit(repo, "add compute_total") |
| 82 | _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items)\n") |
| 83 | _commit(repo, "remove deprecated marker") |
| 84 | |
| 85 | result = _symlog(repo, "grep", "deprecated", "--json") |
| 86 | data = json.loads(result.stdout) |
| 87 | assert data["count"] == 1 |
| 88 | # @{1} is the older entry (newest-first indexing). |
| 89 | assert data["matches"][0]["index"] == 1 |
| 90 | |
| 91 | def test_no_match_returns_empty(self, tmp_path: pathlib.Path) -> None: |
| 92 | repo = _make_repo(tmp_path) |
| 93 | _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items)\n") |
| 94 | _commit(repo, "add compute_total") |
| 95 | |
| 96 | result = _symlog(repo, "grep", "nonexistent_token_xyz", "--json") |
| 97 | data = json.loads(result.stdout) |
| 98 | assert data["count"] == 0 |
| 99 | assert data["matches"] == [] |
| 100 | |
| 101 | def test_case_insensitive_match(self, tmp_path: pathlib.Path) -> None: |
| 102 | repo = _make_repo(tmp_path) |
| 103 | _write_file(repo, "src/billing.py", "def compute_total(items):\n # DEPRECATED\n return sum(items)\n") |
| 104 | _commit(repo, "add compute_total") |
| 105 | |
| 106 | result = _symlog(repo, "grep", "deprecated", "--json") |
| 107 | data = json.loads(result.stdout) |
| 108 | assert data["count"] == 1 |
| 109 | |
| 110 | def test_scoped_to_file(self, tmp_path: pathlib.Path) -> None: |
| 111 | repo = _make_repo(tmp_path) |
| 112 | _write_file(repo, "src/billing.py", "def compute_total(items):\n # TODO: refactor\n return sum(items)\n") |
| 113 | _write_file(repo, "src/other.py", "def unrelated():\n # TODO: refactor\n pass\n") |
| 114 | _commit(repo, "add two files") |
| 115 | |
| 116 | result = _symlog(repo, "grep", "TODO", "--file", "src/billing.py", "--json") |
| 117 | data = json.loads(result.stdout) |
| 118 | assert data["count"] == 1 |
| 119 | assert data["matches"][0]["symbol"] == "src/billing.py::compute_total" |
| 120 | |
| 121 | def test_missing_pattern_errors(self, tmp_path: pathlib.Path) -> None: |
| 122 | repo = _make_repo(tmp_path) |
| 123 | result = _symlog(repo, "grep", "--json") |
| 124 | assert result.exit_code == 1 |
File History
1 commit
sha256:ce542c4d0f6dacc9520c02d0e3378bf96bc377d118a7402f2bbc8af1749b08e0
feat(code-test): audit cache internals, add cache_hit/--cac…
Sonnet 5
minor
⚠
6 hours ago