gabriel / muse public
test_cmd_checkout.py python
167 lines 6.7 KB
95b86799 feat: add --format json to all porcelain commands for agent-first output Gabriel Cardona <gabriel@tellurstori.com> 2d ago
1 """Comprehensive tests for ``muse checkout``.
2
3 Covers:
4 - Unit: branch creation, branch switch, detached HEAD
5 - Integration: working-tree restore, HEAD pointer updates
6 - E2E: CLI flags (--branch / -b, existing branch, commit-id)
7 - Security: validate_branch_name rejects traversal inputs
8 - Stress: rapid branch creation and switching
9 """
10
11 from __future__ import annotations
12
13 import datetime
14 import json
15 import pathlib
16 import uuid
17
18 import pytest
19 from typer.testing import CliRunner
20
21 from muse.cli.app import cli
22
23 runner = CliRunner()
24
25
26 # ---------------------------------------------------------------------------
27 # Shared helpers
28 # ---------------------------------------------------------------------------
29
30 def _env(root: pathlib.Path) -> dict[str, str]:
31 return {"MUSE_REPO_ROOT": str(root)}
32
33
34 def _init_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]:
35 muse_dir = tmp_path / ".muse"
36 muse_dir.mkdir()
37 repo_id = str(uuid.uuid4())
38 (muse_dir / "repo.json").write_text(json.dumps({
39 "repo_id": repo_id,
40 "domain": "midi",
41 "default_branch": "main",
42 "created_at": "2025-01-01T00:00:00+00:00",
43 }), encoding="utf-8")
44 (muse_dir / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
45 (muse_dir / "refs" / "heads").mkdir(parents=True)
46 (muse_dir / "snapshots").mkdir()
47 (muse_dir / "commits").mkdir()
48 (muse_dir / "objects").mkdir()
49 return tmp_path, repo_id
50
51
52 def _make_commit(root: pathlib.Path, repo_id: str, message: str = "test",
53 branch: str = "main") -> str:
54 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
55 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
56
57 ref_file = root / ".muse" / "refs" / "heads" / branch
58 parent_id = ref_file.read_text().strip() if ref_file.exists() else None
59 manifest: dict[str, str] = {}
60 snap_id = compute_snapshot_id(manifest)
61 committed_at = datetime.datetime.now(datetime.timezone.utc)
62 commit_id = compute_commit_id(
63 parent_ids=[parent_id] if parent_id else [],
64 snapshot_id=snap_id, message=message,
65 committed_at_iso=committed_at.isoformat(),
66 )
67 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
68 write_commit(root, CommitRecord(
69 commit_id=commit_id, repo_id=repo_id, branch=branch,
70 snapshot_id=snap_id, message=message, committed_at=committed_at,
71 parent_commit_id=parent_id,
72 ))
73 ref_file.parent.mkdir(parents=True, exist_ok=True)
74 ref_file.write_text(commit_id, encoding="utf-8")
75 return commit_id
76
77
78 # ---------------------------------------------------------------------------
79 # Unit / Integration tests
80 # ---------------------------------------------------------------------------
81
82 class TestCheckoutUnit:
83 def test_create_new_branch(self, tmp_path: pathlib.Path) -> None:
84 root, repo_id = _init_repo(tmp_path)
85 _make_commit(root, repo_id)
86 result = runner.invoke(cli, ["checkout", "-b", "feature"], env=_env(root), catch_exceptions=False)
87 assert result.exit_code == 0
88 head = (root / ".muse" / "HEAD").read_text()
89 assert "feature" in head
90
91 def test_switch_to_existing_branch(self, tmp_path: pathlib.Path) -> None:
92 root, repo_id = _init_repo(tmp_path)
93 commit_id = _make_commit(root, repo_id)
94 (root / ".muse" / "refs" / "heads" / "dev").write_text(commit_id)
95 result = runner.invoke(cli, ["checkout", "dev"], env=_env(root), catch_exceptions=False)
96 assert result.exit_code == 0
97 head = (root / ".muse" / "HEAD").read_text()
98 assert "dev" in head
99
100 def test_switch_by_commit_id(self, tmp_path: pathlib.Path) -> None:
101 root, repo_id = _init_repo(tmp_path)
102 commit_id = _make_commit(root, repo_id)
103 result = runner.invoke(cli, ["checkout", commit_id], env=_env(root), catch_exceptions=False)
104 assert result.exit_code == 0
105
106 def test_checkout_nonexistent_branch_fails(self, tmp_path: pathlib.Path) -> None:
107 root, repo_id = _init_repo(tmp_path)
108 _make_commit(root, repo_id)
109 result = runner.invoke(cli, ["checkout", "nonexistent"], env=_env(root))
110 assert result.exit_code != 0
111
112 def test_create_branch_from_commit(self, tmp_path: pathlib.Path) -> None:
113 root, repo_id = _init_repo(tmp_path)
114 commit_id = _make_commit(root, repo_id)
115 result = runner.invoke(cli, ["checkout", "-b", "new-branch"], env=_env(root), catch_exceptions=False)
116 assert result.exit_code == 0
117 ref_path = root / ".muse" / "refs" / "heads" / "new-branch"
118 assert ref_path.exists()
119 assert ref_path.read_text().strip() == commit_id
120
121
122 class TestCheckoutSecurity:
123 def test_invalid_branch_name_dotdot_rejected(self, tmp_path: pathlib.Path) -> None:
124 root, repo_id = _init_repo(tmp_path)
125 _make_commit(root, repo_id)
126 result = runner.invoke(cli, ["checkout", "../traversal"], env=_env(root))
127 assert result.exit_code != 0
128
129 def test_invalid_branch_name_slash_only_rejected(self, tmp_path: pathlib.Path) -> None:
130 root, repo_id = _init_repo(tmp_path)
131 _make_commit(root, repo_id)
132 result = runner.invoke(cli, ["checkout", "/etc/passwd"], env=_env(root))
133 assert result.exit_code != 0
134
135 def test_create_branch_traversal_rejected(self, tmp_path: pathlib.Path) -> None:
136 root, repo_id = _init_repo(tmp_path)
137 _make_commit(root, repo_id)
138 result = runner.invoke(cli, ["checkout", "-b", "../../evil"], env=_env(root))
139 assert result.exit_code != 0
140
141 def test_ansi_in_output_sanitized(self, tmp_path: pathlib.Path) -> None:
142 root, repo_id = _init_repo(tmp_path)
143 _make_commit(root, repo_id)
144 result = runner.invoke(cli, ["checkout", "safe-branch"], env=_env(root))
145 assert "\x1b" not in result.output
146
147
148 class TestCheckoutStress:
149 def test_many_branch_creations(self, tmp_path: pathlib.Path) -> None:
150 root, repo_id = _init_repo(tmp_path)
151 _make_commit(root, repo_id)
152 for i in range(20):
153 result = runner.invoke(
154 cli, ["checkout", "-b", f"branch-{i}"], env=_env(root), catch_exceptions=False
155 )
156 assert result.exit_code == 0
157
158 def test_rapid_branch_switching(self, tmp_path: pathlib.Path) -> None:
159 root, repo_id = _init_repo(tmp_path)
160 commit_id = _make_commit(root, repo_id)
161 for i in range(5):
162 branch = f"branch-{i}"
163 (root / ".muse" / "refs" / "heads" / branch).write_text(commit_id)
164 for i in range(5):
165 branch = f"branch-{i}"
166 result = runner.invoke(cli, ["checkout", branch], env=_env(root), catch_exceptions=False)
167 assert result.exit_code == 0