test_branch_json_schema.py
python
sha256:d11a87833d5fad6059b7662844bf5448a8911a17cce7a51811f71ad394f248eb
bump to v0.2.0rc13
Human
patch
6 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
1 commit
sha256:d11a87833d5fad6059b7662844bf5448a8911a17cce7a51811f71ad394f248eb
bump to v0.2.0rc13
Human
patch
6 days ago