"""muse#53 item 8 -- `muse symlog grep PATTERN [--file path] [--json]`. Searches symbol bodies across their full symlog history for PATTERN, answering "when did this symbol last contain string X?" """ from __future__ import annotations import json import os import pathlib import textwrap from tests.cli_test_helper import CliRunner runner = CliRunner() def _invoke(repo: pathlib.Path, args: list[str]): saved = os.getcwd() try: os.chdir(repo) return runner.invoke(None, args) finally: os.chdir(saved) def _init_repo(repo: pathlib.Path) -> None: repo.mkdir(parents=True, exist_ok=True) _invoke(repo, ["init"]) def _write_file(repo: pathlib.Path, rel: str, content: str) -> None: p = repo / rel p.parent.mkdir(parents=True, exist_ok=True) p.write_text(textwrap.dedent(content)) def _commit(repo: pathlib.Path, msg: str) -> str: _invoke(repo, ["code", "add", "."]) result = _invoke(repo, [ "commit", "-m", msg, "--agent-id", "claude-code", "--model-id", "claude-sonnet-4-6", "--author", "claude-code", "--json", ]) data = json.loads(result.stdout) return data["commit_id"] def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: _init_repo(tmp_path) return tmp_path def _symlog(repo: pathlib.Path, *args: str): return _invoke(repo, ["symlog", *args]) class TestSymlogGrep: def test_finds_pattern_in_current_body(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_file(repo, "src/billing.py", "def compute_total(items):\n # deprecated: use v2\n return sum(items)\n") _commit(repo, "add compute_total") result = _symlog(repo, "grep", "deprecated", "--json") assert result.exit_code == 0, result.stdout data = json.loads(result.stdout) assert data["pattern"] == "deprecated" assert data["count"] == 1 assert data["matches"][0]["symbol"] == "src/billing.py::compute_total" assert data["matches"][0]["index"] == 0 def test_finds_pattern_only_in_older_version(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) # A comment-only diff doesn't change the symbol's AST content_id (no # new symlog entry is written), so use a real statement to ensure two # distinct entries exist. _write_file(repo, "src/billing.py", "def compute_total(items):\n marker = 'deprecated_v1'\n return sum(items)\n") _commit(repo, "add compute_total") _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items)\n") _commit(repo, "remove deprecated marker") result = _symlog(repo, "grep", "deprecated", "--json") data = json.loads(result.stdout) assert data["count"] == 1 # @{1} is the older entry (newest-first indexing). assert data["matches"][0]["index"] == 1 def test_no_match_returns_empty(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_file(repo, "src/billing.py", "def compute_total(items):\n return sum(items)\n") _commit(repo, "add compute_total") result = _symlog(repo, "grep", "nonexistent_token_xyz", "--json") data = json.loads(result.stdout) assert data["count"] == 0 assert data["matches"] == [] def test_case_insensitive_match(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_file(repo, "src/billing.py", "def compute_total(items):\n # DEPRECATED\n return sum(items)\n") _commit(repo, "add compute_total") result = _symlog(repo, "grep", "deprecated", "--json") data = json.loads(result.stdout) assert data["count"] == 1 def test_scoped_to_file(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_file(repo, "src/billing.py", "def compute_total(items):\n # TODO: refactor\n return sum(items)\n") _write_file(repo, "src/other.py", "def unrelated():\n # TODO: refactor\n pass\n") _commit(repo, "add two files") result = _symlog(repo, "grep", "TODO", "--file", "src/billing.py", "--json") data = json.loads(result.stdout) assert data["count"] == 1 assert data["matches"][0]["symbol"] == "src/billing.py::compute_total" def test_missing_pattern_errors(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) result = _symlog(repo, "grep", "--json") assert result.exit_code == 1