"""muse#54 item 10 -- `muse reflog watch [--branch ] [--json]`. Live-tail of reflog entries: a 'snapshot' event per pre-existing entry, then an 'appended' event for each newly written entry, until --once / --timeout / --max-events ends the loop. """ from __future__ import annotations import json import pathlib import pytest from tests.cli_test_helper import CliRunner from muse.core.types import fake_id, Manifest, NULL_COMMIT_ID from muse.core.paths import muse_dir, logs_dir cli = None runner = CliRunner() def _env(root: pathlib.Path) -> Manifest: 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) -> tuple[pathlib.Path, str]: dot_muse = muse_dir(tmp_path) dot_muse.mkdir() repo_id = fake_id("repo") (dot_muse / "repo.json").write_text(json.dumps({ "repo_id": repo_id, "domain": "code", "default_branch": "main", "created_at": "2025-01-01T00:00:00+00:00", }), encoding="utf-8") (dot_muse / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8") (dot_muse / "refs" / "heads").mkdir(parents=True) (dot_muse / "snapshots").mkdir() (dot_muse / "commits").mkdir() (dot_muse / "objects").mkdir() return tmp_path, repo_id def _commit_file(root: pathlib.Path, content: str, message: str) -> str: (root / "f.txt").write_text(content) add = _run(root, "code", "add", "f.txt") assert add.exit_code == 0, add.output commit = _run(root, "commit", "-m", message, "--json") assert commit.exit_code == 0, commit.output return json.loads(commit.output)["commit_id"] def _ndjson_lines(output: str) -> list[dict]: return [json.loads(line) for line in output.splitlines() if line.strip()] class TestReflogWatchOnce: def test_once_emits_snapshot_only(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) c1 = _commit_file(root, "v1\n", "c1") c2 = _commit_file(root, "v2\n", "c2") result = _run(root, "reflog", "watch", "--once", "--json") assert result.exit_code == 0, result.output events = _ndjson_lines(result.output) assert len(events) == 2 assert all(e["event_type"] == "snapshot" for e in events) # Chronological order: oldest first. assert events[0]["new_id"] == c1 assert events[0]["index"] == 1 assert events[1]["new_id"] == c2 assert events[1]["index"] == 0 def test_once_empty_reflog_emits_nothing(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) result = _run(root, "reflog", "watch", "--once", "--json") assert result.exit_code == 0, result.output assert _ndjson_lines(result.output) == [] def test_once_respects_branch_flag(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) c1 = _commit_file(root, "v1\n", "c1") result = _run(root, "reflog", "watch", "--once", "--branch", "main", "--json") assert result.exit_code == 0, result.output events = _ndjson_lines(result.output) assert len(events) == 1 assert events[0]["new_id"] == c1 def test_invalid_branch_name_errors(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) result = _run(root, "reflog", "watch", "--once", "--branch", "../escape", "--json") assert result.exit_code == 1 def test_invalid_poll_interval_errors(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) result = _run(root, "reflog", "watch", "--once", "--poll-interval", "0", "--json") assert result.exit_code == 1 def test_max_events_caps_snapshot(self, tmp_path: pathlib.Path) -> None: root, repo_id = _init_repo(tmp_path) _commit_file(root, "v1\n", "c1") _commit_file(root, "v2\n", "c2") _commit_file(root, "v3\n", "c3") result = _run(root, "reflog", "watch", "--once", "--max-events", "2", "--json") assert result.exit_code == 0, result.output events = _ndjson_lines(result.output) assert len(events) == 2 class TestReflogWatchAppended: def test_appended_event_detected_during_poll(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None: root, repo_id = _init_repo(tmp_path) c1 = _commit_file(root, "v1\n", "c1") c2 = "2" * 64 # fabricated commit id -- no need for a real commit object import muse.cli.commands.reflog as reflog_cmd head_log = logs_dir(root) / "HEAD" call_count = {"n": 0} def _fake_sleep(seconds: float) -> None: call_count["n"] += 1 if call_count["n"] == 1: # Simulate a second entry landing while we're "sleeping" -- # append directly to the log file, no nested CLI invocation. with head_log.open("a", encoding="utf-8") as f: f.write(f"{c1} {c2} gabriel 1800000000 +0000\tcommit: c2\n") monkeypatch.setattr(reflog_cmd.time, "sleep", _fake_sleep) result = _run(root, "reflog", "watch", "--max-events", "2", "--json") assert result.exit_code == 0, result.output events = _ndjson_lines(result.output) assert len(events) == 2 assert events[0]["event_type"] == "snapshot" assert events[0]["new_id"] == c1 assert events[1]["event_type"] == "appended" assert events[1]["new_id"] == f"sha256:{c2}" assert events[1]["operation"] == "commit: c2"