"""TDD tests for ``muse log --branch``/``-b`` (issue #45). ``muse log`` already reads history from an arbitrary branch/commit via the positional ``ref`` argument (``muse log main``), with no working-tree or HEAD side-effects. ``--branch``/``-b`` did not exist as an explicit flag, so any caller reaching for the more self-documenting/git-idiomatic form got ``unrecognized arguments: --branch``. Coverage: - --branch/-b resolves to the same history as the equivalent positional ref - --branch does not touch HEAD or the working branch - --branch composes with --oneline, --max-count, --json - --branch together with a positional ref is rejected as ambiguous """ from __future__ import annotations import json import os import pathlib from tests.cli_test_helper import CliRunner, InvokeResult runner = CliRunner() def _invoke(repo: pathlib.Path, *args: str) -> InvokeResult: from muse.cli.app import main as cli saved = os.getcwd() try: os.chdir(repo) return runner.invoke(cli, list(args)) finally: os.chdir(saved) def _init(repo: pathlib.Path) -> None: repo.mkdir(parents=True, exist_ok=True) _invoke(repo, "init") def _commit(repo: pathlib.Path, msg: str, filename: str | None = None) -> str: fname = filename or f"f_{abs(hash(msg)) % 100000}.py" (repo / fname).write_text(f"# {msg}\n") _invoke(repo, "code", "add", ".") result = _invoke(repo, "commit", "-m", msg, "--json") return json.loads(result.output)["commit_id"] def _checkout(repo: pathlib.Path, branch: str, *, new: bool = False) -> None: args = ["checkout", "-b", branch] if new else ["checkout", branch] _invoke(repo, *args) def _current_branch(repo: pathlib.Path) -> str: result = _invoke(repo, "status", "--json") return json.loads(result.output)["branch"] def _log_json(repo: pathlib.Path, *extra: str) -> list[dict]: result = _invoke(repo, "log", "--json", *extra) return json.loads(result.output)["commits"] def _two_branch_repo(tmp: pathlib.Path) -> pathlib.Path: """main: A -> B; feat/x branches from A, adds C. HEAD ends on main.""" repo = tmp / "repo" _init(repo) _commit(repo, "A") _checkout(repo, "feat/x", new=True) _commit(repo, "C") _checkout(repo, "main") _commit(repo, "B") return repo class TestBranchFlagBasic: def test_branch_flag_matches_positional_ref(self, tmp_path: pathlib.Path) -> None: repo = _two_branch_repo(tmp_path) via_flag = _log_json(repo, "--branch", "feat/x") via_positional = _log_json(repo, "feat/x") assert via_flag == via_positional assert any(c["message"] == "C" for c in via_flag) assert not any(c["message"] == "B" for c in via_flag) def test_short_flag_b(self, tmp_path: pathlib.Path) -> None: repo = _two_branch_repo(tmp_path) result = _log_json(repo, "-b", "feat/x") assert any(c["message"] == "C" for c in result) def test_branch_flag_does_not_touch_head(self, tmp_path: pathlib.Path) -> None: repo = _two_branch_repo(tmp_path) before = _current_branch(repo) _invoke(repo, "log", "--branch", "feat/x", "--json") assert _current_branch(repo) == before == "main" def test_branch_flag_composes_with_oneline_and_max_count( self, tmp_path: pathlib.Path ) -> None: repo = _two_branch_repo(tmp_path) _checkout(repo, "feat/x") _commit(repo, "D") _checkout(repo, "main") result = _invoke( repo, "log", "--branch", "feat/x", "--oneline", "--max-count", "1" ) assert result.exit_code == 0 lines = [l for l in result.output.strip().splitlines() if l.strip()] assert len(lines) == 1 def test_branch_flag_with_json(self, tmp_path: pathlib.Path) -> None: repo = _two_branch_repo(tmp_path) result = _invoke(repo, "log", "--branch", "feat/x", "--json") data = json.loads(result.output) assert data["branch"] == "feat/x" assert any(c["message"] == "C" for c in data["commits"]) class TestBranchFlagAmbiguity: def test_branch_flag_and_positional_ref_together_is_rejected( self, tmp_path: pathlib.Path ) -> None: repo = _two_branch_repo(tmp_path) result = _invoke(repo, "log", "--branch", "feat/x", "main", "--json") assert result.exit_code != 0