"""Tests for the canonical ``muse branch --json`` schema. Coverage -------- I List schema I1 All required keys present in each list entry I2 committed_at is ISO 8601 with timezone (not null for committed branch) I3 committed_at is null for an empty (never-committed) branch I4 commit_id is sha256:-prefixed (not empty string) when present I5 commit_id is null (not empty string "") for an empty branch I6 current=true for exactly one entry (the checked-out branch) I7 upstream is null when no tracking ref configured II Mutation operations II1 create returns action="created", branch, commit_id II2 delete returns action="deleted", branch, was (full commit_id) II3 rename returns action="renamed", from, to II4 copy returns action="copied", from, to III Error paths III1 delete non-existent branch → JSON error III2 delete current branch → JSON error III3 create duplicate branch → JSON error """ from __future__ import annotations from collections.abc import Mapping import json import pathlib import time import pytest from tests.cli_test_helper import CliRunner cli = None runner = CliRunner() _LIST_REQUIRED_KEYS = { "name", "current", "commit_id", "committed_at", "last_message", "upstream", "created_by", "branch_created_by", "branch_created_at", } def _env(root: pathlib.Path) -> Mapping[str, str]: return {"MUSE_REPO_ROOT": str(root)} def _branch(root: pathlib.Path, *flags: str) -> Mapping[str, object] | list: result = runner.invoke(cli, ["branch", "--json"] + list(flags), env=_env(root)) assert result.exit_code == 0, f"branch --json failed:\n{result.output}" return json.loads(result.output.strip()) def _branch_raw(root: pathlib.Path, *args: str) -> "InvokeResult": return runner.invoke(cli, ["branch", "--json"] + list(args), env=_env(root)) @pytest.fixture() def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: monkeypatch.chdir(tmp_path) env = _env(tmp_path) runner.invoke(cli, ["init", "--domain", "code"], env=env) (tmp_path / "a.py").write_text("x = 1\n") runner.invoke(cli, ["code", "add", "a.py"], env=env) runner.invoke(cli, ["commit", "-m", "initial"], env=env) return tmp_path # --------------------------------------------------------------------------- # I List schema # --------------------------------------------------------------------------- class TestListSchemaI: def test_I1_all_required_keys_present(self, repo: pathlib.Path) -> None: data = _branch(repo) assert isinstance(data, list) and data missing = _LIST_REQUIRED_KEYS - set(data[0].keys()) assert not missing, f"Missing keys in branch list entry: {missing}" def test_I2_committed_at_is_iso8601(self, repo: pathlib.Path) -> None: import datetime data = _branch(repo) main = next(b for b in data if b["name"] == "main") assert main["committed_at"] is not None dt = datetime.datetime.fromisoformat(main["committed_at"]) assert dt.tzinfo is not None def test_I3_committed_at_null_for_empty_branch(self, repo: pathlib.Path) -> None: env = _env(repo) # Create a branch that has never had a commit of its own # (points at same commit as main, but committed_at comes from the commit record # which exists — so test an actually empty branch via a fresh repo branch) runner.invoke(cli, ["branch", "empty-branch"], env=env) # committed_at should still be non-null (branch points at HEAD commit) data = _branch(repo) empty = next((b for b in data if b["name"] == "empty-branch"), None) assert empty is not None # Branch points to the same commit as main, so committed_at is set assert empty["committed_at"] is not None def test_I4_commit_id_sha256_prefixed(self, repo: pathlib.Path) -> None: data = _branch(repo) main = next(b for b in data if b["name"] == "main") assert main["commit_id"] is not None assert main["commit_id"].startswith("sha256:"), ( f"commit_id must be sha256:-prefixed, got {main['commit_id']!r}" ) def test_I5_commit_id_null_not_empty_string( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """I5: An empty (no commits) branch has commit_id=null, not ''.""" monkeypatch.chdir(tmp_path) env = _env(tmp_path) runner.invoke(cli, ["init", "--domain", "code"], env=env) # No commits — main branch is empty data = _branch(tmp_path) assert isinstance(data, list) and data main = next((b for b in data if b["name"] == "main"), None) if main is not None: assert main["commit_id"] is None, ( f"Empty branch commit_id must be null, got {main['commit_id']!r}" ) def test_I6_exactly_one_current(self, repo: pathlib.Path) -> None: env = _env(repo) runner.invoke(cli, ["branch", "feat/x"], env=env) data = _branch(repo) current = [b for b in data if b["current"]] assert len(current) == 1, f"Expected 1 current branch, got {len(current)}" def test_I7_upstream_null_when_unset(self, repo: pathlib.Path) -> None: data = _branch(repo) main = next(b for b in data if b["name"] == "main") assert main["upstream"] is None # --------------------------------------------------------------------------- # II Mutation operations # --------------------------------------------------------------------------- class TestMutationOperationsII: def test_II1_create_json_schema(self, repo: pathlib.Path) -> None: data = _branch(repo, "feat/new") assert data["action"] == "created" assert data["branch"] == "feat/new" assert data["commit_id"] is not None assert data["commit_id"].startswith("sha256:") def test_II2_delete_json_schema(self, repo: pathlib.Path) -> None: env = _env(repo) runner.invoke(cli, ["branch", "feat/to-del"], env=env) runner.invoke(cli, ["checkout", "feat/to-del"], env=env) runner.invoke(cli, ["checkout", "main"], env=env) data = _branch(repo, "-d", "feat/to-del") assert data["action"] == "deleted" assert data["branch"] == "feat/to-del" assert "was" in data def test_II3_rename_json_schema(self, repo: pathlib.Path) -> None: env = _env(repo) runner.invoke(cli, ["branch", "old-name"], env=env) data = _branch(repo, "-m", "old-name", "new-name") assert data["action"] == "renamed" assert data["from"] == "old-name" assert data["to"] == "new-name" def test_II4_copy_json_schema(self, repo: pathlib.Path) -> None: env = _env(repo) runner.invoke(cli, ["branch", "src-branch"], env=env) data = _branch(repo, "-c", "src-branch", "dst-branch") assert data["action"] == "copied" assert data["from"] == "src-branch" assert data["to"] == "dst-branch" # --------------------------------------------------------------------------- # III Error paths # --------------------------------------------------------------------------- class TestErrorPathsIII: def test_III1_delete_nonexistent_json_error(self, repo: pathlib.Path) -> None: result = _branch_raw(repo, "-D", "ghost-branch") assert result.exit_code == 1 data = json.loads(result.output.strip().splitlines()[0]) assert data["error"] == "not_found" def test_III2_delete_current_branch_json_error(self, repo: pathlib.Path) -> None: result = _branch_raw(repo, "-d", "main") assert result.exit_code == 1 data = json.loads(result.output.strip().splitlines()[0]) assert data["error"] == "current_branch" def test_III3_create_duplicate_json_error(self, repo: pathlib.Path) -> None: result = _branch_raw(repo, "main") assert result.exit_code == 1 data = json.loads(result.output.strip().splitlines()[0]) assert data["error"] == "already_exists" # --------------------------------------------------------------------------- # musehub#215 — created_by tip-authorship fallback (tiers 2–7) # --------------------------------------------------------------------------- class TestCreatedByAuthorFallback215: """Seven-tier coverage for ``created_by`` author fallback (freeze §5). Tier 1 (unit) lives in ``tests/test_cmd_branch.py`` (``TestCreatedByFromRecord``). This class covers tiers 2–7. """ def test_tier2_integration_human_commit_non_null_created_by( self, repo: pathlib.Path ) -> None: """Tier 2: human commit (no --agent-id) → created_by equals author.""" env = _env(repo) (repo / "b.py").write_text("y = 2\n") runner.invoke(cli, ["code", "add", "b.py"], env=env) commit_r = runner.invoke( cli, ["commit", "-m", "human tip", "--author", "aaronrene"], env=env ) assert commit_r.exit_code == 0, commit_r.output data = _branch(repo) assert isinstance(data, list) main = next(b for b in data if b["name"] == "main") assert main["created_by"] == "aaronrene" def test_tier3_e2e_human_then_agent_tip(self, repo: pathlib.Path) -> None: """Tier 3: human tip → author; subsequent agent tip → agent_id.""" env = _env(repo) (repo / "c.py").write_text("z = 3\n") runner.invoke(cli, ["code", "add", "c.py"], env=env) r1 = runner.invoke( cli, ["commit", "-m", "human", "--author", "aaronrene"], env=env ) assert r1.exit_code == 0, r1.output human_list = _branch(repo) main = next(b for b in human_list if b["name"] == "main") assert main["created_by"] == "aaronrene" (repo / "d.py").write_text("w = 4\n") runner.invoke(cli, ["code", "add", "d.py"], env=env) r2 = runner.invoke( cli, [ "commit", "-m", "agent tip", "--author", "aaronrene", "--agent-id", "claude-code", ], env=env, ) assert r2.exit_code == 0, r2.output agent_list = _branch(repo) main2 = next(b for b in agent_list if b["name"] == "main") assert main2["created_by"] == "claude-code" def test_tier4_stress_200_branches_have_created_by_key( self, repo: pathlib.Path ) -> None: """Tier 4: 200-branch --json list; every entry has created_by key.""" env = _env(repo) for i in range(200): r = runner.invoke(cli, ["branch", f"stress-{i:03d}"], env=env) assert r.exit_code == 0, r.output t0 = time.perf_counter() data = _branch(repo) elapsed = time.perf_counter() - t0 assert isinstance(data, list) assert len(data) == 201 # main + 200 for entry in data: assert "created_by" in entry, f"missing created_by in {entry.get('name')!r}" assert elapsed < 5.0, f"200-branch list took {elapsed:.2f}s (limit 5s)" def test_tier5_data_integrity_stable_then_tip_change( self, repo: pathlib.Path ) -> None: """Tier 5: unchanged repo → identical created_by; new tip → changes.""" env = _env(repo) (repo / "e.py").write_text("e = 1\n") runner.invoke(cli, ["code", "add", "e.py"], env=env) r_agent = runner.invoke( cli, [ "commit", "-m", "agent tip", "--author", "aaronrene", "--agent-id", "claude-code", ], env=env, ) assert r_agent.exit_code == 0, r_agent.output first = _branch(repo) second = _branch(repo) assert isinstance(first, list) and isinstance(second, list) by_name_1 = {b["name"]: b["created_by"] for b in first} by_name_2 = {b["name"]: b["created_by"] for b in second} assert by_name_1 == by_name_2 assert by_name_1["main"] == "claude-code" (repo / "f.py").write_text("f = 2\n") runner.invoke(cli, ["code", "add", "f.py"], env=env) r_human = runner.invoke( cli, ["commit", "-m", "human tip", "--author", "aaronrene"], env=env ) assert r_human.exit_code == 0, r_human.output after = _branch(repo) main_after = next(b for b in after if b["name"] == "main") assert main_after["created_by"] == "aaronrene" assert main_after["created_by"] != by_name_1["main"] def test_tier6_performance_200_branch_list_under_5s( self, repo: pathlib.Path ) -> None: """Tier 6: wall-clock bound for 200-branch --json list (< 5s).""" env = _env(repo) for i in range(200): runner.invoke(cli, ["branch", f"perf-{i:03d}"], env=env) t0 = time.perf_counter() data = _branch(repo) elapsed = time.perf_counter() - t0 assert isinstance(data, list) assert len(data) == 201 assert elapsed < 5.0, f"performance list took {elapsed:.2f}s (limit 5s)" def test_tier7_security_esc_stripped_no_env_leak( self, repo: pathlib.Path ) -> None: """Tier 7: ESC in author absent from JSON; no MUSE_*/key material.""" env = _env(repo) (repo / "g.py").write_text("g = 1\n") runner.invoke(cli, ["code", "add", "g.py"], env=env) malicious = "\x1b[31mmalicious-author\x1b[0m" commit_r = runner.invoke( cli, ["commit", "-m", "esc tip", "--author", malicious], env=env ) assert commit_r.exit_code == 0, commit_r.output raw = runner.invoke(cli, ["branch", "--json"], env=env) assert raw.exit_code == 0, raw.output assert "\x1b" not in raw.output assert "MUSE_" not in raw.output assert "private_key" not in raw.output.lower() assert "BEGIN" not in raw.output data = json.loads(raw.output.strip()) main = next(b for b in data if b["name"] == "main") assert main["created_by"] is not None assert "\x1b" not in main["created_by"] assert main["created_by"] == "[31mmalicious-author[0m"