gabriel / muse public
test_symlog_remote_sharing.py python
330 lines 12.8 KB
Raw
sha256:b570925a487b44187c0441977d252906b6bf2719253bc53f96412d520cd24d9e feat: muse#53 item 4 — remote symlog sharing via push/pull/bundle Sonnet 5 minor ⚠ breaking 8 hours ago
1 """muse#53 item 4 -- remote symlog sharing.
2
3 Covers:
4 - core: symlog_entries_to_wire() / merge_remote_symlog_entries()
5 - CLI: `muse push --symlog` sends every local symbol's journal entries,
6 `muse pull` fetches and merges them directly into local journals,
7 `muse bundle create`/`unbundle` carry symlogs too.
8 """
9 from __future__ import annotations
10
11 import datetime
12 import json
13 import os
14 import pathlib
15 from unittest.mock import MagicMock, patch
16
17 import pytest
18
19 from muse.core.types import fake_id
20 from muse.core.paths import muse_dir
21 from muse.core.symlog import (
22 NULL_CONTENT_ID,
23 SymlogEntry,
24 merge_remote_symlog_entries,
25 read_symlog,
26 symlog_entries_to_wire,
27 symlog_path,
28 )
29 from tests.cli_test_helper import CliRunner
30
31 cli = None
32 runner = CliRunner()
33
34
35 def _env(root: pathlib.Path) -> dict:
36 return {"MUSE_REPO_ROOT": str(root)}
37
38
39 def _run(root: pathlib.Path, *args: str):
40 return runner.invoke(cli, list(args), env=_env(root))
41
42
43 def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path:
44 saved = os.getcwd()
45 try:
46 os.chdir(tmp_path)
47 r = _run(tmp_path, "init", "--domain", "code")
48 finally:
49 os.chdir(saved)
50 assert r.exit_code == 0, r.output
51 (muse_dir(tmp_path) / "config.toml").write_text(
52 '[remotes.local]\nurl = "https://localhost:1337/gabriel/myrepo"\n',
53 encoding="utf-8",
54 )
55 return tmp_path
56
57
58 def _write(repo: pathlib.Path, rel: str, content: str) -> None:
59 (repo / rel).write_text(content)
60
61
62 def _commit(repo: pathlib.Path, msg: str) -> str:
63 add = _run(repo, "code", "add", ".")
64 assert add.exit_code == 0, add.output
65 commit = _run(repo, "commit", "-m", msg, "--json")
66 assert commit.exit_code == 0, commit.output
67 return json.loads(commit.output)["commit_id"]
68
69
70 def _fake_id(n: int) -> str:
71 return f"sha256:{n:064x}"
72
73
74 def _mock_push_transport(local_head: str) -> MagicMock:
75 from muse.core.mpack import RemoteInfo
76 transport = MagicMock()
77 transport.fetch_remote_info.return_value = RemoteInfo(
78 domain="code", default_branch="main", branch_heads={},
79 )
80 transport.push_mpack_presign.return_value = {
81 "upload_url": "https://minio.example.com/put?sig=x",
82 "mpack_key": "sha256:" + "a" * 64,
83 }
84 transport.push_mpack_put.return_value = None
85 transport.push_mpack_unpack.return_value = {
86 "job_id": "j", "head": local_head, "branch": "main",
87 "blobs_in_mpack": 0, "commits_in_mpack": 1,
88 }
89 return transport
90
91
92 class TestSymlogEntriesToWire:
93 def test_passes_through_canonical_ids(self) -> None:
94 entry = SymlogEntry(
95 old_content_id=NULL_CONTENT_ID,
96 new_content_id="sha256:" + "a" * 64,
97 commit_id="sha256:" + "c" * 64,
98 author="gabriel",
99 timestamp=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
100 operation="symbol-created: compute_total",
101 )
102 wire = symlog_entries_to_wire([entry])
103 assert len(wire) == 1
104 assert wire[0]["old_content_id"] == NULL_CONTENT_ID
105 assert wire[0]["new_content_id"] == "sha256:" + "a" * 64
106 assert wire[0]["commit_id"] == "sha256:" + "c" * 64
107 assert wire[0]["operation"] == "symbol-created: compute_total"
108
109
110 class TestMergeRemoteSymlogEntries:
111 def test_merges_new_entries_into_local_journal(self, tmp_path: pathlib.Path) -> None:
112 repo = _init_repo(tmp_path)
113 wire_entries = [{
114 "old_content_id": NULL_CONTENT_ID, "new_content_id": _fake_id(1),
115 "commit_id": _fake_id(100), "author": "gabriel",
116 "timestamp": "2026-01-01T00:00:00+00:00",
117 "operation": "symbol-created: compute_total",
118 }]
119 merged = merge_remote_symlog_entries(repo, "billing.py::compute_total", wire_entries)
120 assert merged == 1
121
122 entries = read_symlog(repo, "billing.py::compute_total")
123 assert len(entries) == 1
124 assert entries[0].new_content_id == _fake_id(1)
125
126 def test_idempotent_on_repeat_merge(self, tmp_path: pathlib.Path) -> None:
127 repo = _init_repo(tmp_path)
128 wire_entries = [{
129 "old_content_id": NULL_CONTENT_ID, "new_content_id": _fake_id(1),
130 "commit_id": _fake_id(100), "author": "gabriel",
131 "timestamp": "2026-01-01T00:00:00+00:00",
132 "operation": "symbol-created: compute_total",
133 }]
134 first = merge_remote_symlog_entries(repo, "billing.py::compute_total", wire_entries)
135 second = merge_remote_symlog_entries(repo, "billing.py::compute_total", wire_entries)
136 assert first == 1
137 assert second == 0
138
139 entries = read_symlog(repo, "billing.py::compute_total")
140 assert len(entries) == 1
141
142 def test_separate_symbols_do_not_collide(self, tmp_path: pathlib.Path) -> None:
143 repo = _init_repo(tmp_path)
144 merge_remote_symlog_entries(repo, "billing.py::a", [{
145 "old_content_id": NULL_CONTENT_ID, "new_content_id": _fake_id(1),
146 "commit_id": _fake_id(100), "author": "alice",
147 "timestamp": "2026-01-01T00:00:00+00:00", "operation": "symbol-created: a",
148 }])
149 merge_remote_symlog_entries(repo, "billing.py::b", [{
150 "old_content_id": NULL_CONTENT_ID, "new_content_id": _fake_id(2),
151 "commit_id": _fake_id(100), "author": "bob",
152 "timestamp": "2026-01-01T00:00:00+00:00", "operation": "symbol-created: b",
153 }])
154
155 a_entries = read_symlog(repo, "billing.py::a")
156 b_entries = read_symlog(repo, "billing.py::b")
157 assert len(a_entries) == 1
158 assert len(b_entries) == 1
159 assert a_entries[0].new_content_id == _fake_id(1)
160 assert b_entries[0].new_content_id == _fake_id(2)
161
162 def test_empty_entries_is_noop(self, tmp_path: pathlib.Path) -> None:
163 repo = _init_repo(tmp_path)
164 assert merge_remote_symlog_entries(repo, "billing.py::compute_total", []) == 0
165
166
167 class TestPushSymlogFlag:
168 def test_push_with_symlog_calls_transport(
169 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch,
170 ) -> None:
171 repo = _init_repo(tmp_path)
172 _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n")
173 commit_id = _commit(repo, "add compute_total")
174 monkeypatch.setenv("MUSE_REPO_ROOT", str(repo))
175 monkeypatch.chdir(repo)
176
177 transport = _mock_push_transport(commit_id)
178 transport.push_symlog.return_value = {"stored": 1, "skipped": 0}
179
180 with patch("muse.cli.commands.push.make_transport", return_value=transport):
181 result = _run(repo, "push", "local", "main", "--symlog", "--json")
182
183 assert result.exit_code == 0, result.output
184 data = json.loads(result.output)
185 assert data["symlog_pushed"] == 1
186 transport.push_symlog.assert_called_once()
187 call_args = transport.push_symlog.call_args
188 entries_by_symbol = call_args[0][2]
189 assert "billing.py::compute_total" in entries_by_symbol
190
191 def test_push_without_symlog_flag_does_not_call_transport(
192 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch,
193 ) -> None:
194 repo = _init_repo(tmp_path)
195 _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n")
196 commit_id = _commit(repo, "add compute_total")
197 monkeypatch.setenv("MUSE_REPO_ROOT", str(repo))
198 monkeypatch.chdir(repo)
199
200 transport = _mock_push_transport(commit_id)
201
202 with patch("muse.cli.commands.push.make_transport", return_value=transport):
203 result = _run(repo, "push", "local", "main", "--json")
204
205 assert result.exit_code == 0, result.output
206 data = json.loads(result.output)
207 assert data["symlog_pushed"] == 0
208 assert data["symlog_skipped"] == 0
209 transport.push_symlog.assert_not_called()
210
211 def test_push_symlog_config_flag(
212 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch,
213 ) -> None:
214 repo = _init_repo(tmp_path)
215 _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n")
216 commit_id = _commit(repo, "add compute_total")
217 monkeypatch.setenv("MUSE_REPO_ROOT", str(repo))
218 monkeypatch.chdir(repo)
219
220 config_result = _run(repo, "config", "set", "push.symlog", "true", "--json")
221 assert config_result.exit_code == 0, config_result.output
222
223 transport = _mock_push_transport(commit_id)
224 transport.push_symlog.return_value = {"stored": 1, "skipped": 0}
225
226 with patch("muse.cli.commands.push.make_transport", return_value=transport):
227 result = _run(repo, "push", "local", "main", "--json")
228
229 assert result.exit_code == 0, result.output
230 transport.push_symlog.assert_called_once()
231
232
233 class TestPullFetchesSymlog:
234 def test_pull_merges_symlog_entries(
235 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch,
236 ) -> None:
237 repo = _init_repo(tmp_path)
238 _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n")
239 commit_id = _commit(repo, "add compute_total")
240 monkeypatch.setenv("MUSE_REPO_ROOT", str(repo))
241 monkeypatch.chdir(repo)
242
243 from muse.core.mpack import RemoteInfo
244
245 transport = MagicMock()
246 transport.fetch_remote_info.return_value = RemoteInfo(
247 domain="code", default_branch="main", branch_heads={"main": commit_id},
248 )
249 transport.fetch_mpack.return_value = {
250 "repo_id": "repo1", "domain": "code", "default_branch": "main",
251 "branch_heads": {"main": commit_id},
252 "commits": [], "snapshots": [], "blobs": [],
253 "blobs_received": 0, "shallow_commits": [],
254 }
255 transport.fetch_version_tags.return_value = {"tags": []}
256 transport.fetch_reflog.return_value = {"entries": []}
257 transport.fetch_symlog.return_value = {"symbols": {
258 "billing.py::other_fn": [{
259 "old_content_id": NULL_CONTENT_ID, "new_content_id": _fake_id(1),
260 "commit_id": commit_id, "author": "gabriel",
261 "timestamp": "2026-01-01T00:00:00+00:00",
262 "operation": "symbol-created: other_fn",
263 }],
264 }}
265
266 with patch("muse.cli.commands.pull.make_transport", return_value=transport):
267 result = _run(repo, "pull", "local", "main", "--json")
268
269 assert result.exit_code == 0, result.output
270 transport.fetch_symlog.assert_called_once()
271
272 entries = read_symlog(repo, "billing.py::other_fn")
273 assert len(entries) == 1
274 assert entries[0].new_content_id == _fake_id(1)
275
276 def test_pull_symlog_fetch_failure_is_non_fatal(
277 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch,
278 ) -> None:
279 repo = _init_repo(tmp_path)
280 _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n")
281 commit_id = _commit(repo, "add compute_total")
282 monkeypatch.setenv("MUSE_REPO_ROOT", str(repo))
283 monkeypatch.chdir(repo)
284
285 from muse.core.mpack import RemoteInfo
286 from muse.core.transport import TransportError
287
288 transport = MagicMock()
289 transport.fetch_remote_info.return_value = RemoteInfo(
290 domain="code", default_branch="main", branch_heads={"main": commit_id},
291 )
292 transport.fetch_mpack.return_value = {
293 "repo_id": "repo1", "domain": "code", "default_branch": "main",
294 "branch_heads": {"main": commit_id},
295 "commits": [], "snapshots": [], "blobs": [],
296 "blobs_received": 0, "shallow_commits": [],
297 }
298 transport.fetch_version_tags.return_value = {"tags": []}
299 transport.fetch_reflog.return_value = {"entries": []}
300 transport.fetch_symlog.side_effect = TransportError("boom", 500)
301
302 with patch("muse.cli.commands.pull.make_transport", return_value=transport):
303 result = _run(repo, "pull", "local", "main", "--json")
304
305 assert result.exit_code == 0, result.output
306
307
308 class TestBundleCarriesSymlog:
309 def test_bundle_create_and_unbundle_round_trips_symlog(self, tmp_path: pathlib.Path) -> None:
310 src = tmp_path / "src"
311 dst = tmp_path / "dst"
312 src.mkdir()
313 dst.mkdir()
314
315 src_repo = _init_repo(src)
316 _write(src_repo, "billing.py", "def compute_total(items):\n return sum(items)\n")
317 _commit(src_repo, "add compute_total")
318
319 bundle_file = tmp_path / "repo.bundle"
320 create_result = _run(src_repo, "bundle", "create", str(bundle_file), "--json")
321 assert create_result.exit_code == 0, create_result.output
322
323 dst_repo = _init_repo(dst)
324 unbundle_result = _run(dst_repo, "bundle", "unbundle", str(bundle_file), "--json")
325 assert unbundle_result.exit_code == 0, unbundle_result.output
326 data = json.loads(unbundle_result.output)
327 assert data["symlog_entries_merged"] >= 1
328
329 entries = read_symlog(dst_repo, "billing.py::compute_total")
330 assert len(entries) >= 1
File History 1 commit
sha256:b570925a487b44187c0441977d252906b6bf2719253bc53f96412d520cd24d9e feat: muse#53 item 4 — remote symlog sharing via push/pull/bundle Sonnet 5 minor ⚠ 8 hours ago