"""muse#106 — ``muse switch`` must work from a detached HEAD, like git. git allows switching back to a branch (or creating one, or using ``-``) from detached HEAD with zero special ceremony. ``muse switch`` did not: it called the strict ``read_current_branch()`` unguarded, which raises ``ValueError`` on detached HEAD, producing a raw Python traceback instead of a clean switch or a structured error. Coverage: reproduce the crash (RED before the fix), then every case the ticket's acceptance criteria call out: switch to an existing branch, switch to a nonexistent branch, ``-c`` from detached, ``-C`` from detached, and ``--json`` mode never emitting a raw traceback. """ from __future__ import annotations import json import os import pathlib import pytest from tests.cli_test_helper import CliRunner, InvokeResult from muse.core.refs import get_head_commit_id, read_head runner = CliRunner() def _run(repo: pathlib.Path, *args: str) -> InvokeResult: saved = os.getcwd() try: os.chdir(repo) return runner.invoke(None, list(args)) finally: os.chdir(saved) @pytest.fixture() def detached_repo(tmp_path: pathlib.Path) -> pathlib.Path: """Repo with two commits on main, HEAD detached at the first commit.""" _run(tmp_path, "init") (tmp_path / "a.py").write_text("x = 1\n") _run(tmp_path, "code", "add", "a.py") _run(tmp_path, "commit", "-m", "first") first = get_head_commit_id(tmp_path, "main") (tmp_path / "a.py").write_text("x = 2\n") _run(tmp_path, "code", "add", "a.py") _run(tmp_path, "commit", "-m", "second") result = _run(tmp_path, "switch", str(first), "--detach") assert result.exit_code == 0, result.output head = read_head(tmp_path) assert head["kind"] == "commit", "fixture must start detached" return tmp_path def test_switch_to_existing_branch_from_detached_head_succeeds(detached_repo: pathlib.Path) -> None: """The exact repro from the ticket: must switch cleanly, not crash.""" result = _run(detached_repo, "switch", "main") assert result.exit_code == 0, result.output assert "Traceback" not in result.output assert "Traceback" not in result.stderr head = read_head(detached_repo) assert head["kind"] == "branch" assert head["branch"] == "main" def test_switch_to_existing_branch_from_detached_head_json(detached_repo: pathlib.Path) -> None: result = _run(detached_repo, "switch", "main", "--json") assert result.exit_code == 0, result.output assert "Traceback" not in result.output assert "Traceback" not in result.stderr payload = json.loads(result.output) assert payload["action"] == "switched" assert payload["branch"] == "main" assert payload["from_branch"] is None def test_switch_to_nonexistent_branch_from_detached_head_clean_error(detached_repo: pathlib.Path) -> None: """Must fail cleanly (structured error), never a raw traceback.""" result = _run(detached_repo, "switch", "does-not-exist", "--json") assert "Traceback" not in result.output assert "Traceback" not in result.stderr assert result.exit_code != 0 payload = json.loads(result.output) assert "error" in payload def test_switch_create_from_detached_head_branches_off_current_commit(detached_repo: pathlib.Path) -> None: """``switch -c`` from detached HEAD creates the branch at the detached commit, matching git's behavior of branching off wherever HEAD points.""" head = read_head(detached_repo) detached_commit = head["commit_id"] result = _run(detached_repo, "switch", "-c", "new-branch") assert result.exit_code == 0, result.output assert "Traceback" not in result.output assert "Traceback" not in result.stderr new_head = read_head(detached_repo) assert new_head["kind"] == "branch" assert new_head["branch"] == "new-branch" assert get_head_commit_id(detached_repo, "new-branch") == detached_commit def test_switch_force_create_from_detached_head(detached_repo: pathlib.Path) -> None: """``switch -C`` from detached HEAD must not crash either (same root cause, same code pattern, not explicitly called out in the ticket's acceptance list but sharing the exact same unguarded read).""" result = _run(detached_repo, "switch", "-C", "force-branch") assert result.exit_code == 0, result.output assert "Traceback" not in result.output assert "Traceback" not in result.stderr def test_switch_dash_from_detached_head_returns_to_real_previous_branch(detached_repo: pathlib.Path) -> None: """``switch -`` must not write the literal string "None" as a branch name into PREV_BRANCH while detached -- it should return to the last real branch the user was on before detaching.""" result = _run(detached_repo, "switch", "main") assert result.exit_code == 0, result.output first = get_head_commit_id(detached_repo, "main") detach = _run(detached_repo, "switch", first, "--detach") assert detach.exit_code == 0, detach.output result = _run(detached_repo, "switch", "-") assert result.exit_code == 0, result.output assert "Traceback" not in result.output assert "Traceback" not in result.stderr head = read_head(detached_repo) assert head["kind"] == "branch" assert head["branch"] == "main" def test_checkout_recovery_path_still_works(detached_repo: pathlib.Path) -> None: """The existing working recovery (muse checkout ) must be unaffected by this fix.""" result = _run(detached_repo, "checkout", "main") assert result.exit_code == 0, result.output head = read_head(detached_repo) assert head["kind"] == "branch" assert head["branch"] == "main"