gabriel / muse public
test_branch_json_schema.py python
201 lines 7.9 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 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 from collections.abc import Mapping
28
29 import json
30 import pathlib
31
32 import pytest
33
34 from tests.cli_test_helper import CliRunner
35
36 cli = None
37 runner = CliRunner()
38
39 _LIST_REQUIRED_KEYS = {
40 "name", "current", "commit_id", "committed_at", "last_message", "upstream",
41 }
42
43
44 def _env(root: pathlib.Path) -> Mapping[str, str]:
45 return {"MUSE_REPO_ROOT": str(root)}
46
47
48 def _branch(root: pathlib.Path, *flags: str) -> Mapping[str, object] | list:
49 result = runner.invoke(cli, ["branch", "--json"] + list(flags), env=_env(root))
50 assert result.exit_code == 0, f"branch --json failed:\n{result.output}"
51 return json.loads(result.output.strip())
52
53
54 def _branch_raw(root: pathlib.Path, *args: str) -> "InvokeResult":
55 return runner.invoke(cli, ["branch", "--json"] + list(args), env=_env(root))
56
57
58 @pytest.fixture()
59 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
60 monkeypatch.chdir(tmp_path)
61 env = _env(tmp_path)
62 runner.invoke(cli, ["init", "--domain", "code"], env=env)
63 (tmp_path / "a.py").write_text("x = 1\n")
64 runner.invoke(cli, ["code", "add", "a.py"], env=env)
65 runner.invoke(cli, ["commit", "-m", "initial"], env=env)
66 return tmp_path
67
68
69 # ---------------------------------------------------------------------------
70 # I List schema
71 # ---------------------------------------------------------------------------
72
73
74 class TestListSchemaI:
75 def test_I1_all_required_keys_present(self, repo: pathlib.Path) -> None:
76 data = _branch(repo)
77 assert isinstance(data, list) and data
78 missing = _LIST_REQUIRED_KEYS - set(data[0].keys())
79 assert not missing, f"Missing keys in branch list entry: {missing}"
80
81 def test_I2_committed_at_is_iso8601(self, repo: pathlib.Path) -> None:
82 import datetime
83 data = _branch(repo)
84 main = next(b for b in data if b["name"] == "main")
85 assert main["committed_at"] is not None
86 dt = datetime.datetime.fromisoformat(main["committed_at"])
87 assert dt.tzinfo is not None
88
89 def test_I3_committed_at_null_for_empty_branch(self, repo: pathlib.Path) -> None:
90 env = _env(repo)
91 # Create a branch that has never had a commit of its own
92 # (points at same commit as main, but committed_at comes from the commit record
93 # which exists — so test an actually empty branch via a fresh repo branch)
94 runner.invoke(cli, ["branch", "empty-branch"], env=env)
95 # committed_at should still be non-null (branch points at HEAD commit)
96 data = _branch(repo)
97 empty = next((b for b in data if b["name"] == "empty-branch"), None)
98 assert empty is not None
99 # Branch points to the same commit as main, so committed_at is set
100 assert empty["committed_at"] is not None
101
102 def test_I4_commit_id_sha256_prefixed(self, repo: pathlib.Path) -> None:
103 data = _branch(repo)
104 main = next(b for b in data if b["name"] == "main")
105 assert main["commit_id"] is not None
106 assert main["commit_id"].startswith("sha256:"), (
107 f"commit_id must be sha256:-prefixed, got {main['commit_id']!r}"
108 )
109
110 def test_I5_commit_id_null_not_empty_string(
111 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
112 ) -> None:
113 """I5: An empty (no commits) branch has commit_id=null, not ''."""
114 monkeypatch.chdir(tmp_path)
115 env = _env(tmp_path)
116 runner.invoke(cli, ["init", "--domain", "code"], env=env)
117 # No commits — main branch is empty
118 data = _branch(tmp_path)
119 assert isinstance(data, list) and data
120 main = next((b for b in data if b["name"] == "main"), None)
121 if main is not None:
122 assert main["commit_id"] is None, (
123 f"Empty branch commit_id must be null, got {main['commit_id']!r}"
124 )
125
126 def test_I6_exactly_one_current(self, repo: pathlib.Path) -> None:
127 env = _env(repo)
128 runner.invoke(cli, ["branch", "feat/x"], env=env)
129 data = _branch(repo)
130 current = [b for b in data if b["current"]]
131 assert len(current) == 1, f"Expected 1 current branch, got {len(current)}"
132
133 def test_I7_upstream_null_when_unset(self, repo: pathlib.Path) -> None:
134 data = _branch(repo)
135 main = next(b for b in data if b["name"] == "main")
136 assert main["upstream"] is None
137
138
139 # ---------------------------------------------------------------------------
140 # II Mutation operations
141 # ---------------------------------------------------------------------------
142
143
144 class TestMutationOperationsII:
145 def test_II1_create_json_schema(self, repo: pathlib.Path) -> None:
146 data = _branch(repo, "feat/new")
147 assert data["action"] == "created"
148 assert data["branch"] == "feat/new"
149 assert data["commit_id"] is not None
150 assert data["commit_id"].startswith("sha256:")
151
152 def test_II2_delete_json_schema(self, repo: pathlib.Path) -> None:
153 env = _env(repo)
154 runner.invoke(cli, ["branch", "feat/to-del"], env=env)
155 runner.invoke(cli, ["checkout", "feat/to-del"], env=env)
156 runner.invoke(cli, ["checkout", "main"], env=env)
157 data = _branch(repo, "-d", "feat/to-del")
158 assert data["action"] == "deleted"
159 assert data["branch"] == "feat/to-del"
160 assert "was" in data
161
162 def test_II3_rename_json_schema(self, repo: pathlib.Path) -> None:
163 env = _env(repo)
164 runner.invoke(cli, ["branch", "old-name"], env=env)
165 data = _branch(repo, "-m", "old-name", "new-name")
166 assert data["action"] == "renamed"
167 assert data["from"] == "old-name"
168 assert data["to"] == "new-name"
169
170 def test_II4_copy_json_schema(self, repo: pathlib.Path) -> None:
171 env = _env(repo)
172 runner.invoke(cli, ["branch", "src-branch"], env=env)
173 data = _branch(repo, "-c", "src-branch", "dst-branch")
174 assert data["action"] == "copied"
175 assert data["from"] == "src-branch"
176 assert data["to"] == "dst-branch"
177
178
179 # ---------------------------------------------------------------------------
180 # III Error paths
181 # ---------------------------------------------------------------------------
182
183
184 class TestErrorPathsIII:
185 def test_III1_delete_nonexistent_json_error(self, repo: pathlib.Path) -> None:
186 result = _branch_raw(repo, "-D", "ghost-branch")
187 assert result.exit_code == 1
188 data = json.loads(result.output.strip().splitlines()[0])
189 assert data["error"] == "not_found"
190
191 def test_III2_delete_current_branch_json_error(self, repo: pathlib.Path) -> None:
192 result = _branch_raw(repo, "-d", "main")
193 assert result.exit_code == 1
194 data = json.loads(result.output.strip().splitlines()[0])
195 assert data["error"] == "current_branch"
196
197 def test_III3_create_duplicate_json_error(self, repo: pathlib.Path) -> None:
198 result = _branch_raw(repo, "main")
199 assert result.exit_code == 1
200 data = json.loads(result.output.strip().splitlines()[0])
201 assert data["error"] == "already_exists"
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 29 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago