"""muse#53 item 4 -- remote symlog sharing. Covers: - core: symlog_entries_to_wire() / merge_remote_symlog_entries() - CLI: `muse push --symlog` sends every local symbol's journal entries, `muse pull` fetches and merges them directly into local journals, `muse bundle create`/`unbundle` carry symlogs too. """ from __future__ import annotations import datetime import json import os import pathlib from unittest.mock import MagicMock, patch import pytest from muse.core.types import fake_id from muse.core.paths import muse_dir from muse.core.symlog import ( NULL_CONTENT_ID, SymlogEntry, merge_remote_symlog_entries, read_symlog, symlog_entries_to_wire, symlog_path, ) from tests.cli_test_helper import CliRunner cli = None runner = CliRunner() def _env(root: pathlib.Path) -> dict: return {"MUSE_REPO_ROOT": str(root)} def _run(root: pathlib.Path, *args: str): return runner.invoke(cli, list(args), env=_env(root)) def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path: saved = os.getcwd() try: os.chdir(tmp_path) r = _run(tmp_path, "init", "--domain", "code") finally: os.chdir(saved) assert r.exit_code == 0, r.output (muse_dir(tmp_path) / "config.toml").write_text( '[remotes.local]\nurl = "https://localhost:1337/gabriel/myrepo"\n', encoding="utf-8", ) return tmp_path def _write(repo: pathlib.Path, rel: str, content: str) -> None: (repo / rel).write_text(content) def _commit(repo: pathlib.Path, msg: str) -> str: add = _run(repo, "code", "add", ".") assert add.exit_code == 0, add.output commit = _run(repo, "commit", "-m", msg, "--json") assert commit.exit_code == 0, commit.output return json.loads(commit.output)["commit_id"] def _fake_id(n: int) -> str: return f"sha256:{n:064x}" def _mock_push_transport(local_head: str) -> MagicMock: from muse.core.mpack import RemoteInfo transport = MagicMock() transport.fetch_remote_info.return_value = RemoteInfo( domain="code", default_branch="main", branch_heads={}, ) transport.push_mpack_presign.return_value = { "upload_url": "https://minio.example.com/put?sig=x", "mpack_key": "sha256:" + "a" * 64, } transport.push_mpack_put.return_value = None transport.push_mpack_unpack.return_value = { "job_id": "j", "head": local_head, "branch": "main", "blobs_in_mpack": 0, "commits_in_mpack": 1, } return transport class TestSymlogEntriesToWire: def test_passes_through_canonical_ids(self) -> None: entry = SymlogEntry( old_content_id=NULL_CONTENT_ID, new_content_id="sha256:" + "a" * 64, commit_id="sha256:" + "c" * 64, author="gabriel", timestamp=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc), operation="symbol-created: compute_total", ) wire = symlog_entries_to_wire([entry]) assert len(wire) == 1 assert wire[0]["old_content_id"] == NULL_CONTENT_ID assert wire[0]["new_content_id"] == "sha256:" + "a" * 64 assert wire[0]["commit_id"] == "sha256:" + "c" * 64 assert wire[0]["operation"] == "symbol-created: compute_total" class TestMergeRemoteSymlogEntries: def test_merges_new_entries_into_local_journal(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) wire_entries = [{ "old_content_id": NULL_CONTENT_ID, "new_content_id": _fake_id(1), "commit_id": _fake_id(100), "author": "gabriel", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "symbol-created: compute_total", }] merged = merge_remote_symlog_entries(repo, "billing.py::compute_total", wire_entries) assert merged == 1 entries = read_symlog(repo, "billing.py::compute_total") assert len(entries) == 1 assert entries[0].new_content_id == _fake_id(1) def test_idempotent_on_repeat_merge(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) wire_entries = [{ "old_content_id": NULL_CONTENT_ID, "new_content_id": _fake_id(1), "commit_id": _fake_id(100), "author": "gabriel", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "symbol-created: compute_total", }] first = merge_remote_symlog_entries(repo, "billing.py::compute_total", wire_entries) second = merge_remote_symlog_entries(repo, "billing.py::compute_total", wire_entries) assert first == 1 assert second == 0 entries = read_symlog(repo, "billing.py::compute_total") assert len(entries) == 1 def test_separate_symbols_do_not_collide(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) merge_remote_symlog_entries(repo, "billing.py::a", [{ "old_content_id": NULL_CONTENT_ID, "new_content_id": _fake_id(1), "commit_id": _fake_id(100), "author": "alice", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "symbol-created: a", }]) merge_remote_symlog_entries(repo, "billing.py::b", [{ "old_content_id": NULL_CONTENT_ID, "new_content_id": _fake_id(2), "commit_id": _fake_id(100), "author": "bob", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "symbol-created: b", }]) a_entries = read_symlog(repo, "billing.py::a") b_entries = read_symlog(repo, "billing.py::b") assert len(a_entries) == 1 assert len(b_entries) == 1 assert a_entries[0].new_content_id == _fake_id(1) assert b_entries[0].new_content_id == _fake_id(2) def test_empty_entries_is_noop(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) assert merge_remote_symlog_entries(repo, "billing.py::compute_total", []) == 0 class TestPushSymlogFlag: def test_push_with_symlog_calls_transport( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, ) -> None: repo = _init_repo(tmp_path) _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n") commit_id = _commit(repo, "add compute_total") monkeypatch.setenv("MUSE_REPO_ROOT", str(repo)) monkeypatch.chdir(repo) transport = _mock_push_transport(commit_id) transport.push_symlog.return_value = {"stored": 1, "skipped": 0} with patch("muse.cli.commands.push.make_transport", return_value=transport): result = _run(repo, "push", "local", "main", "--symlog", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["symlog_pushed"] == 1 transport.push_symlog.assert_called_once() call_args = transport.push_symlog.call_args entries_by_symbol = call_args[0][2] assert "billing.py::compute_total" in entries_by_symbol def test_push_without_symlog_flag_does_not_call_transport( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, ) -> None: repo = _init_repo(tmp_path) _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n") commit_id = _commit(repo, "add compute_total") monkeypatch.setenv("MUSE_REPO_ROOT", str(repo)) monkeypatch.chdir(repo) transport = _mock_push_transport(commit_id) with patch("muse.cli.commands.push.make_transport", return_value=transport): result = _run(repo, "push", "local", "main", "--json") assert result.exit_code == 0, result.output data = json.loads(result.output) assert data["symlog_pushed"] == 0 assert data["symlog_skipped"] == 0 transport.push_symlog.assert_not_called() def test_push_symlog_config_flag( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, ) -> None: repo = _init_repo(tmp_path) _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n") commit_id = _commit(repo, "add compute_total") monkeypatch.setenv("MUSE_REPO_ROOT", str(repo)) monkeypatch.chdir(repo) config_result = _run(repo, "config", "set", "push.symlog", "true", "--json") assert config_result.exit_code == 0, config_result.output transport = _mock_push_transport(commit_id) transport.push_symlog.return_value = {"stored": 1, "skipped": 0} with patch("muse.cli.commands.push.make_transport", return_value=transport): result = _run(repo, "push", "local", "main", "--json") assert result.exit_code == 0, result.output transport.push_symlog.assert_called_once() class TestPullFetchesSymlog: def test_pull_merges_symlog_entries( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, ) -> None: repo = _init_repo(tmp_path) _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n") commit_id = _commit(repo, "add compute_total") monkeypatch.setenv("MUSE_REPO_ROOT", str(repo)) monkeypatch.chdir(repo) from muse.core.mpack import RemoteInfo transport = MagicMock() transport.fetch_remote_info.return_value = RemoteInfo( domain="code", default_branch="main", branch_heads={"main": commit_id}, ) transport.fetch_mpack.return_value = { "repo_id": "repo1", "domain": "code", "default_branch": "main", "branch_heads": {"main": commit_id}, "commits": [], "snapshots": [], "blobs": [], "blobs_received": 0, "shallow_commits": [], } transport.fetch_version_tags.return_value = {"tags": []} transport.fetch_reflog.return_value = {"entries": []} transport.fetch_symlog.return_value = {"symbols": { "billing.py::other_fn": [{ "old_content_id": NULL_CONTENT_ID, "new_content_id": _fake_id(1), "commit_id": commit_id, "author": "gabriel", "timestamp": "2026-01-01T00:00:00+00:00", "operation": "symbol-created: other_fn", }], }} with patch("muse.cli.commands.pull.make_transport", return_value=transport): result = _run(repo, "pull", "local", "main", "--json") assert result.exit_code == 0, result.output transport.fetch_symlog.assert_called_once() entries = read_symlog(repo, "billing.py::other_fn") assert len(entries) == 1 assert entries[0].new_content_id == _fake_id(1) def test_pull_symlog_fetch_failure_is_non_fatal( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, ) -> None: repo = _init_repo(tmp_path) _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n") commit_id = _commit(repo, "add compute_total") monkeypatch.setenv("MUSE_REPO_ROOT", str(repo)) monkeypatch.chdir(repo) from muse.core.mpack import RemoteInfo from muse.core.transport import TransportError transport = MagicMock() transport.fetch_remote_info.return_value = RemoteInfo( domain="code", default_branch="main", branch_heads={"main": commit_id}, ) transport.fetch_mpack.return_value = { "repo_id": "repo1", "domain": "code", "default_branch": "main", "branch_heads": {"main": commit_id}, "commits": [], "snapshots": [], "blobs": [], "blobs_received": 0, "shallow_commits": [], } transport.fetch_version_tags.return_value = {"tags": []} transport.fetch_reflog.return_value = {"entries": []} transport.fetch_symlog.side_effect = TransportError("boom", 500) with patch("muse.cli.commands.pull.make_transport", return_value=transport): result = _run(repo, "pull", "local", "main", "--json") assert result.exit_code == 0, result.output class TestBundleCarriesSymlog: def test_bundle_create_and_unbundle_round_trips_symlog(self, tmp_path: pathlib.Path) -> None: src = tmp_path / "src" dst = tmp_path / "dst" src.mkdir() dst.mkdir() src_repo = _init_repo(src) _write(src_repo, "billing.py", "def compute_total(items):\n return sum(items)\n") _commit(src_repo, "add compute_total") bundle_file = tmp_path / "repo.bundle" create_result = _run(src_repo, "bundle", "create", str(bundle_file), "--json") assert create_result.exit_code == 0, create_result.output dst_repo = _init_repo(dst) unbundle_result = _run(dst_repo, "bundle", "unbundle", str(bundle_file), "--json") assert unbundle_result.exit_code == 0, unbundle_result.output data = json.loads(unbundle_result.output) assert data["symlog_entries_merged"] >= 1 entries = read_symlog(dst_repo, "billing.py::compute_total") assert len(entries) >= 1