gabriel / muse public
test_plumbing_snapshot_diff.py python
194 lines 7.2 KB
Raw
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 74 days ago
1 """Tests for ``muse plumbing snapshot-diff``.
2
3 Verifies categorisation of added/modified/deleted paths, resolution of
4 snapshot IDs, commit IDs, and branch names, text-format output, and error
5 handling for unresolvable refs.
6 """
7
8 from __future__ import annotations
9
10 import datetime
11 import hashlib
12 import json
13 import pathlib
14
15 from tests.cli_test_helper import CliRunner
16
17 cli = None # argparse migration — CliRunner ignores this arg
18 from muse.core.errors import ExitCode
19 from muse.core.object_store import write_object
20 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
21 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
22 from muse.core._types import Manifest
23
24 runner = CliRunner()
25
26
27 # ---------------------------------------------------------------------------
28 # Helpers
29 # ---------------------------------------------------------------------------
30
31
32 def _sha(data: bytes | str) -> str:
33 raw = data if isinstance(data, bytes) else data.encode()
34 return hashlib.sha256(raw).hexdigest()
35
36
37 def _init_repo(path: pathlib.Path) -> pathlib.Path:
38 muse = path / ".muse"
39 (muse / "commits").mkdir(parents=True)
40 (muse / "snapshots").mkdir(parents=True)
41 (muse / "objects").mkdir(parents=True)
42 (muse / "refs" / "heads").mkdir(parents=True)
43 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
44 (muse / "repo.json").write_text(
45 json.dumps({"repo_id": "test-repo", "domain": "midi"}), encoding="utf-8"
46 )
47 return path
48
49
50 def _env(repo: pathlib.Path) -> Manifest:
51 return {"MUSE_REPO_ROOT": str(repo)}
52
53
54 def _obj(repo: pathlib.Path, content: bytes) -> str:
55 oid = _sha(content)
56 write_object(repo, oid, content)
57 return oid
58
59
60 def _snap(repo: pathlib.Path, manifest: Manifest) -> str:
61 sid = compute_snapshot_id(manifest)
62 write_snapshot(
63 repo,
64 SnapshotRecord(
65 snapshot_id=sid,
66 manifest=manifest,
67 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
68 ),
69 )
70 return sid
71
72
73 def _commit(repo: pathlib.Path, tag: str, sid: str, branch: str = "main") -> str:
74 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
75 cid = compute_commit_id([], sid, tag, committed_at.isoformat())
76 write_commit(
77 repo,
78 CommitRecord(
79 commit_id=cid,
80 repo_id="test-repo",
81 branch=branch,
82 snapshot_id=sid,
83 message=tag,
84 committed_at=committed_at,
85 author="tester",
86 parent_commit_id=None,
87 ),
88 )
89 ref = repo / ".muse" / "refs" / "heads" / branch
90 ref.write_text(cid, encoding="utf-8")
91 return cid
92
93
94 # ---------------------------------------------------------------------------
95 # Tests
96 # ---------------------------------------------------------------------------
97
98
99 class TestSnapshotDiff:
100 def test_added_deleted_categorised_correctly(self, tmp_path: pathlib.Path) -> None:
101 repo = _init_repo(tmp_path)
102 shared = _obj(repo, b"shared")
103 new_obj = _obj(repo, b"new")
104 sid_a = _snap(repo, {"shared.mid": shared, "old.mid": shared})
105 sid_b = _snap(repo, {"shared.mid": shared, "new.mid": new_obj})
106 result = runner.invoke(cli, ["snapshot-diff", sid_a, sid_b], env=_env(repo))
107 assert result.exit_code == 0, result.output
108 data = json.loads(result.stdout)
109 assert [e["path"] for e in data["added"]] == ["new.mid"]
110 assert [e["path"] for e in data["deleted"]] == ["old.mid"]
111 assert data["modified"] == []
112 assert data["total_changes"] == 2
113
114 def test_modified_entry_contains_both_object_ids(self, tmp_path: pathlib.Path) -> None:
115 repo = _init_repo(tmp_path)
116 v1 = _obj(repo, b"v1")
117 v2 = _obj(repo, b"v2")
118 sid_a = _snap(repo, {"track.mid": v1})
119 sid_b = _snap(repo, {"track.mid": v2})
120 result = runner.invoke(cli, ["snapshot-diff", sid_a, sid_b], env=_env(repo))
121 assert result.exit_code == 0, result.output
122 data = json.loads(result.stdout)
123 assert len(data["modified"]) == 1
124 mod = data["modified"][0]
125 assert mod["path"] == "track.mid"
126 assert mod["object_id_a"] == v1
127 assert mod["object_id_b"] == v2
128
129 def test_zero_changes_when_snapshots_identical(self, tmp_path: pathlib.Path) -> None:
130 repo = _init_repo(tmp_path)
131 obj = _obj(repo, b"same")
132 sid = _snap(repo, {"f.mid": obj})
133 result = runner.invoke(cli, ["snapshot-diff", sid, sid], env=_env(repo))
134 assert result.exit_code == 0, result.output
135 data = json.loads(result.stdout)
136 assert data["total_changes"] == 0
137
138 def test_resolves_by_branch_name(self, tmp_path: pathlib.Path) -> None:
139 repo = _init_repo(tmp_path)
140 obj_a = _obj(repo, b"a")
141 obj_b = _obj(repo, b"b")
142 _commit(repo, "cmt-main", _snap(repo, {"a.mid": obj_a}), branch="main")
143 _commit(repo, "cmt-dev", _snap(repo, {"b.mid": obj_b}), branch="dev")
144 (repo / ".muse" / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
145 result = runner.invoke(cli, ["snapshot-diff", "main", "dev"], env=_env(repo))
146 assert result.exit_code == 0, result.output
147 data = json.loads(result.stdout)
148 assert data["total_changes"] == 2
149
150 def test_text_format_shows_status_letters(self, tmp_path: pathlib.Path) -> None:
151 repo = _init_repo(tmp_path)
152 shared = _obj(repo, b"s")
153 new_obj = _obj(repo, b"n")
154 sid_a = _snap(repo, {"gone.mid": shared})
155 sid_b = _snap(repo, {"new.mid": new_obj})
156 result = runner.invoke(
157 cli, ["snapshot-diff", "--format", "text", sid_a, sid_b], env=_env(repo)
158 )
159 assert result.exit_code == 0, result.output
160 assert "A new.mid" in result.stdout
161 assert "D gone.mid" in result.stdout
162
163 def test_stat_flag_appends_summary(self, tmp_path: pathlib.Path) -> None:
164 repo = _init_repo(tmp_path)
165 sid_a = _snap(repo, {"gone.mid": _obj(repo, b"g")})
166 sid_b = _snap(repo, {"new.mid": _obj(repo, b"n")})
167 result = runner.invoke(
168 cli,
169 ["snapshot-diff", "--format", "text", "--stat", sid_a, sid_b],
170 env=_env(repo),
171 )
172 assert result.exit_code == 0, result.output
173 assert "added" in result.stdout
174 assert "deleted" in result.stdout
175
176 def test_unresolvable_ref_exits_user_error(self, tmp_path: pathlib.Path) -> None:
177 repo = _init_repo(tmp_path)
178 result = runner.invoke(
179 cli, ["snapshot-diff", "no-such-thing", "also-missing"], env=_env(repo)
180 )
181 assert result.exit_code == ExitCode.USER_ERROR
182 assert "error" in json.loads(result.stdout)
183
184 def test_results_sorted_lexicographically(self, tmp_path: pathlib.Path) -> None:
185 repo = _init_repo(tmp_path)
186 sid_a = _snap(repo, {})
187 sid_b = _snap(
188 repo, {"z.mid": _obj(repo, b"z"), "a.mid": _obj(repo, b"a"), "m.mid": _obj(repo, b"m")}
189 )
190 result = runner.invoke(cli, ["snapshot-diff", sid_a, sid_b], env=_env(repo))
191 assert result.exit_code == 0, result.output
192 data = json.loads(result.stdout)
193 added_paths = [e["path"] for e in data["added"]]
194 assert added_paths == sorted(added_paths)
File History 1 commit
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 74 days ago