gabriel / muse public
test_branch_json_schema.py python
200 lines 7.8 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 142 days ago
1 """Tests for the canonical ``muse branch --json`` schema.
2
3 Coverage
4 --------
5 I List schema
6 I1 All required keys present in each list entry
7 I2 committed_at is ISO 8601 with timezone (not null for committed branch)
8 I3 committed_at is null for an empty (never-committed) branch
9 I4 commit_id is sha256:-prefixed (not empty string) when present
10 I5 commit_id is null (not empty string "") for an empty branch
11 I6 current=true for exactly one entry (the checked-out branch)
12 I7 upstream is null when no tracking ref configured
13
14 II Mutation operations
15 II1 create returns action="created", branch, commit_id
16 II2 delete returns action="deleted", branch, was (full commit_id)
17 II3 rename returns action="renamed", from, to
18 II4 copy returns action="copied", from, to
19
20 III Error paths
21 III1 delete non-existent branch → JSON error
22 III2 delete current branch → JSON error
23 III3 create duplicate branch → JSON error
24 """
25
26 from __future__ import annotations
27
28 import json
29 import pathlib
30
31 import pytest
32
33 from tests.cli_test_helper import CliRunner
34
35 cli = None
36 runner = CliRunner()
37
38 _LIST_REQUIRED_KEYS = {
39 "name", "current", "commit_id", "committed_at", "last_message", "upstream",
40 }
41
42
43 def _env(root: pathlib.Path) -> dict[str, str]:
44 return {"MUSE_REPO_ROOT": str(root)}
45
46
47 def _branch(root: pathlib.Path, *flags: str) -> dict | list:
48 result = runner.invoke(cli, ["branch", "--json"] + list(flags), env=_env(root))
49 assert result.exit_code == 0, f"branch --json failed:\n{result.output}"
50 return json.loads(result.output.strip())
51
52
53 def _branch_raw(root: pathlib.Path, *args: str):
54 return runner.invoke(cli, ["branch", "--json"] + list(args), env=_env(root))
55
56
57 @pytest.fixture()
58 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
59 monkeypatch.chdir(tmp_path)
60 env = _env(tmp_path)
61 runner.invoke(cli, ["init", "--domain", "code"], env=env)
62 (tmp_path / "a.py").write_text("x = 1\n")
63 runner.invoke(cli, ["code", "add", "a.py"], env=env)
64 runner.invoke(cli, ["commit", "-m", "initial"], env=env)
65 return tmp_path
66
67
68 # ---------------------------------------------------------------------------
69 # I List schema
70 # ---------------------------------------------------------------------------
71
72
73 class TestListSchemaI:
74 def test_I1_all_required_keys_present(self, repo: pathlib.Path) -> None:
75 data = _branch(repo)
76 assert isinstance(data, list) and data
77 missing = _LIST_REQUIRED_KEYS - set(data[0].keys())
78 assert not missing, f"Missing keys in branch list entry: {missing}"
79
80 def test_I2_committed_at_is_iso8601(self, repo: pathlib.Path) -> None:
81 import datetime
82 data = _branch(repo)
83 main = next(b for b in data if b["name"] == "main")
84 assert main["committed_at"] is not None
85 dt = datetime.datetime.fromisoformat(main["committed_at"])
86 assert dt.tzinfo is not None
87
88 def test_I3_committed_at_null_for_empty_branch(self, repo: pathlib.Path) -> None:
89 env = _env(repo)
90 # Create a branch that has never had a commit of its own
91 # (points at same commit as main, but committed_at comes from the commit record
92 # which exists — so test an actually empty branch via a fresh repo branch)
93 runner.invoke(cli, ["branch", "empty-branch"], env=env)
94 # committed_at should still be non-null (branch points at HEAD commit)
95 data = _branch(repo)
96 empty = next((b for b in data if b["name"] == "empty-branch"), None)
97 assert empty is not None
98 # Branch points to the same commit as main, so committed_at is set
99 assert empty["committed_at"] is not None
100
101 def test_I4_commit_id_sha256_prefixed(self, repo: pathlib.Path) -> None:
102 data = _branch(repo)
103 main = next(b for b in data if b["name"] == "main")
104 assert main["commit_id"] is not None
105 assert main["commit_id"].startswith("sha256:"), (
106 f"commit_id must be sha256:-prefixed, got {main['commit_id']!r}"
107 )
108
109 def test_I5_commit_id_null_not_empty_string(
110 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
111 ) -> None:
112 """I5: An empty (no commits) branch has commit_id=null, not ''."""
113 monkeypatch.chdir(tmp_path)
114 env = _env(tmp_path)
115 runner.invoke(cli, ["init", "--domain", "code"], env=env)
116 # No commits — main branch is empty
117 data = _branch(tmp_path)
118 assert isinstance(data, list) and data
119 main = next((b for b in data if b["name"] == "main"), None)
120 if main is not None:
121 assert main["commit_id"] is None, (
122 f"Empty branch commit_id must be null, got {main['commit_id']!r}"
123 )
124
125 def test_I6_exactly_one_current(self, repo: pathlib.Path) -> None:
126 env = _env(repo)
127 runner.invoke(cli, ["branch", "feat/x"], env=env)
128 data = _branch(repo)
129 current = [b for b in data if b["current"]]
130 assert len(current) == 1, f"Expected 1 current branch, got {len(current)}"
131
132 def test_I7_upstream_null_when_unset(self, repo: pathlib.Path) -> None:
133 data = _branch(repo)
134 main = next(b for b in data if b["name"] == "main")
135 assert main["upstream"] is None
136
137
138 # ---------------------------------------------------------------------------
139 # II Mutation operations
140 # ---------------------------------------------------------------------------
141
142
143 class TestMutationOperationsII:
144 def test_II1_create_json_schema(self, repo: pathlib.Path) -> None:
145 data = _branch(repo, "feat/new")
146 assert data["action"] == "created"
147 assert data["branch"] == "feat/new"
148 assert data["commit_id"] is not None
149 assert data["commit_id"].startswith("sha256:")
150
151 def test_II2_delete_json_schema(self, repo: pathlib.Path) -> None:
152 env = _env(repo)
153 runner.invoke(cli, ["branch", "feat/to-del"], env=env)
154 runner.invoke(cli, ["checkout", "feat/to-del"], env=env)
155 runner.invoke(cli, ["checkout", "main"], env=env)
156 data = _branch(repo, "-d", "feat/to-del")
157 assert data["action"] == "deleted"
158 assert data["branch"] == "feat/to-del"
159 assert "was" in data
160
161 def test_II3_rename_json_schema(self, repo: pathlib.Path) -> None:
162 env = _env(repo)
163 runner.invoke(cli, ["branch", "old-name"], env=env)
164 data = _branch(repo, "-m", "old-name", "new-name")
165 assert data["action"] == "renamed"
166 assert data["from"] == "old-name"
167 assert data["to"] == "new-name"
168
169 def test_II4_copy_json_schema(self, repo: pathlib.Path) -> None:
170 env = _env(repo)
171 runner.invoke(cli, ["branch", "src-branch"], env=env)
172 data = _branch(repo, "-c", "src-branch", "dst-branch")
173 assert data["action"] == "copied"
174 assert data["from"] == "src-branch"
175 assert data["to"] == "dst-branch"
176
177
178 # ---------------------------------------------------------------------------
179 # III Error paths
180 # ---------------------------------------------------------------------------
181
182
183 class TestErrorPathsIII:
184 def test_III1_delete_nonexistent_json_error(self, repo: pathlib.Path) -> None:
185 result = _branch_raw(repo, "-D", "ghost-branch")
186 assert result.exit_code == 1
187 data = json.loads(result.output.strip().splitlines()[0])
188 assert data["error"] == "not_found"
189
190 def test_III2_delete_current_branch_json_error(self, repo: pathlib.Path) -> None:
191 result = _branch_raw(repo, "-d", "main")
192 assert result.exit_code == 1
193 data = json.loads(result.output.strip().splitlines()[0])
194 assert data["error"] == "current_branch"
195
196 def test_III3_create_duplicate_json_error(self, repo: pathlib.Path) -> None:
197 result = _branch_raw(repo, "main")
198 assert result.exit_code == 1
199 data = json.loads(result.output.strip().splitlines()[0])
200 assert data["error"] == "already_exists"
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 142 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 145 days ago