test_log_branch_flag.py
python
sha256:24d9cb81ac1dd071e5d98862c740ad101180588361b4f96a790af79a99aed901
docs: architectural plan for unified-store snapshot-content…
Sonnet 5
9 days ago
| 1 | """TDD tests for ``muse log --branch``/``-b`` (issue #45). |
| 2 | |
| 3 | ``muse log`` already reads history from an arbitrary branch/commit via the |
| 4 | positional ``ref`` argument (``muse log main``), with no working-tree or |
| 5 | HEAD side-effects. ``--branch``/``-b`` did not exist as an explicit flag, |
| 6 | so any caller reaching for the more self-documenting/git-idiomatic form |
| 7 | got ``unrecognized arguments: --branch``. |
| 8 | |
| 9 | Coverage: |
| 10 | - --branch/-b resolves to the same history as the equivalent positional ref |
| 11 | - --branch does not touch HEAD or the working branch |
| 12 | - --branch composes with --oneline, --max-count, --json |
| 13 | - --branch together with a positional ref is rejected as ambiguous |
| 14 | """ |
| 15 | from __future__ import annotations |
| 16 | |
| 17 | import json |
| 18 | import os |
| 19 | import pathlib |
| 20 | |
| 21 | from tests.cli_test_helper import CliRunner, InvokeResult |
| 22 | |
| 23 | runner = CliRunner() |
| 24 | |
| 25 | |
| 26 | def _invoke(repo: pathlib.Path, *args: str) -> InvokeResult: |
| 27 | from muse.cli.app import main as cli |
| 28 | |
| 29 | saved = os.getcwd() |
| 30 | try: |
| 31 | os.chdir(repo) |
| 32 | return runner.invoke(cli, list(args)) |
| 33 | finally: |
| 34 | os.chdir(saved) |
| 35 | |
| 36 | |
| 37 | def _init(repo: pathlib.Path) -> None: |
| 38 | repo.mkdir(parents=True, exist_ok=True) |
| 39 | _invoke(repo, "init") |
| 40 | |
| 41 | |
| 42 | def _commit(repo: pathlib.Path, msg: str, filename: str | None = None) -> str: |
| 43 | fname = filename or f"f_{abs(hash(msg)) % 100000}.py" |
| 44 | (repo / fname).write_text(f"# {msg}\n") |
| 45 | _invoke(repo, "code", "add", ".") |
| 46 | result = _invoke(repo, "commit", "-m", msg, "--json") |
| 47 | return json.loads(result.output)["commit_id"] |
| 48 | |
| 49 | |
| 50 | def _checkout(repo: pathlib.Path, branch: str, *, new: bool = False) -> None: |
| 51 | args = ["checkout", "-b", branch] if new else ["checkout", branch] |
| 52 | _invoke(repo, *args) |
| 53 | |
| 54 | |
| 55 | def _current_branch(repo: pathlib.Path) -> str: |
| 56 | result = _invoke(repo, "status", "--json") |
| 57 | return json.loads(result.output)["branch"] |
| 58 | |
| 59 | |
| 60 | def _log_json(repo: pathlib.Path, *extra: str) -> list[dict]: |
| 61 | result = _invoke(repo, "log", "--json", *extra) |
| 62 | return json.loads(result.output)["commits"] |
| 63 | |
| 64 | |
| 65 | def _two_branch_repo(tmp: pathlib.Path) -> pathlib.Path: |
| 66 | """main: A -> B; feat/x branches from A, adds C. HEAD ends on main.""" |
| 67 | repo = tmp / "repo" |
| 68 | _init(repo) |
| 69 | _commit(repo, "A") |
| 70 | _checkout(repo, "feat/x", new=True) |
| 71 | _commit(repo, "C") |
| 72 | _checkout(repo, "main") |
| 73 | _commit(repo, "B") |
| 74 | return repo |
| 75 | |
| 76 | |
| 77 | class TestBranchFlagBasic: |
| 78 | def test_branch_flag_matches_positional_ref(self, tmp_path: pathlib.Path) -> None: |
| 79 | repo = _two_branch_repo(tmp_path) |
| 80 | |
| 81 | via_flag = _log_json(repo, "--branch", "feat/x") |
| 82 | via_positional = _log_json(repo, "feat/x") |
| 83 | |
| 84 | assert via_flag == via_positional |
| 85 | assert any(c["message"] == "C" for c in via_flag) |
| 86 | assert not any(c["message"] == "B" for c in via_flag) |
| 87 | |
| 88 | def test_short_flag_b(self, tmp_path: pathlib.Path) -> None: |
| 89 | repo = _two_branch_repo(tmp_path) |
| 90 | |
| 91 | result = _log_json(repo, "-b", "feat/x") |
| 92 | |
| 93 | assert any(c["message"] == "C" for c in result) |
| 94 | |
| 95 | def test_branch_flag_does_not_touch_head(self, tmp_path: pathlib.Path) -> None: |
| 96 | repo = _two_branch_repo(tmp_path) |
| 97 | before = _current_branch(repo) |
| 98 | |
| 99 | _invoke(repo, "log", "--branch", "feat/x", "--json") |
| 100 | |
| 101 | assert _current_branch(repo) == before == "main" |
| 102 | |
| 103 | def test_branch_flag_composes_with_oneline_and_max_count( |
| 104 | self, tmp_path: pathlib.Path |
| 105 | ) -> None: |
| 106 | repo = _two_branch_repo(tmp_path) |
| 107 | _checkout(repo, "feat/x") |
| 108 | _commit(repo, "D") |
| 109 | _checkout(repo, "main") |
| 110 | |
| 111 | result = _invoke( |
| 112 | repo, "log", "--branch", "feat/x", "--oneline", "--max-count", "1" |
| 113 | ) |
| 114 | |
| 115 | assert result.exit_code == 0 |
| 116 | lines = [l for l in result.output.strip().splitlines() if l.strip()] |
| 117 | assert len(lines) == 1 |
| 118 | |
| 119 | def test_branch_flag_with_json(self, tmp_path: pathlib.Path) -> None: |
| 120 | repo = _two_branch_repo(tmp_path) |
| 121 | |
| 122 | result = _invoke(repo, "log", "--branch", "feat/x", "--json") |
| 123 | data = json.loads(result.output) |
| 124 | |
| 125 | assert data["branch"] == "feat/x" |
| 126 | assert any(c["message"] == "C" for c in data["commits"]) |
| 127 | |
| 128 | |
| 129 | class TestBranchFlagAmbiguity: |
| 130 | def test_branch_flag_and_positional_ref_together_is_rejected( |
| 131 | self, tmp_path: pathlib.Path |
| 132 | ) -> None: |
| 133 | repo = _two_branch_repo(tmp_path) |
| 134 | |
| 135 | result = _invoke(repo, "log", "--branch", "feat/x", "main", "--json") |
| 136 | |
| 137 | assert result.exit_code != 0 |
File History
1 commit
sha256:24d9cb81ac1dd071e5d98862c740ad101180588361b4f96a790af79a99aed901
docs: architectural plan for unified-store snapshot-content…
Sonnet 5
9 days ago