"""Tests for musehub#192 Phase 4 — discoverability of undeclared/uninstalled hooks at clone time (`muse clone`) and on-demand (`muse status`). Covers the shared `muse.core.hooks.install_notice()` helper directly (real filesystem, no mocking needed) plus its two call sites. """ from __future__ import annotations import os import pathlib from unittest.mock import MagicMock, patch import pytest from muse.core.hooks import install_hooks, install_notice from tests.cli_test_helper import CliRunner, InvokeResult runner = CliRunner() def _invoke_status(path: pathlib.Path, args: list[str]) -> InvokeResult: saved = os.getcwd() try: os.chdir(path) return runner.invoke(None, args) finally: os.chdir(saved) def _init_repo(path: pathlib.Path) -> None: r = _invoke_status(path, ["init"]) assert r.exit_code == 0, r.output def _write_hooks(path: pathlib.Path, content: str) -> None: (path / ".musehooks.toml").write_text(content, encoding="utf-8") # --------------------------------------------------------------------------- # install_notice — shared helper, unit-tested directly # --------------------------------------------------------------------------- class TestInstallNotice: def test_none_when_not_defined(self, tmp_path: pathlib.Path) -> None: assert install_notice(tmp_path) is None def test_message_when_defined_not_installed(self, tmp_path: pathlib.Path) -> None: _write_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') notice = install_notice(tmp_path) assert notice is not None assert "muse hooks install" in notice def test_none_when_installed(self, tmp_path: pathlib.Path) -> None: _write_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') install_hooks(tmp_path) assert install_notice(tmp_path) is None # --------------------------------------------------------------------------- # HK_31 — muse status prints the notice # --------------------------------------------------------------------------- class TestStatusNotice: def test_status_prints_notice_when_defined_not_installed(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) _write_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') r = _invoke_status(tmp_path, ["status"]) assert "muse hooks install" in r.stderr def test_status_silent_when_no_hooks_defined(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) r = _invoke_status(tmp_path, ["status"]) assert "muse hooks install" not in r.stderr def test_status_silent_when_already_installed(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) _write_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') _invoke_status(tmp_path, ["hooks", "install"]) r = _invoke_status(tmp_path, ["status"]) assert "muse hooks install" not in r.stderr def test_status_json_mode_still_silent_on_stdout(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) _write_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') r = _invoke_status(tmp_path, ["status", "--json"]) assert "muse hooks install" not in r.output # --------------------------------------------------------------------------- # HK_30 — muse clone prints the notice for a repo that ships .musehooks.toml # --------------------------------------------------------------------------- def _make_transport_mock() -> MagicMock: from muse.core.mpack import RemoteInfo from muse.core.types import long_id t = MagicMock() commit_id = long_id("a" * 64) t.fetch_remote_info.return_value = RemoteInfo( repo_id="test-repo-id", domain="code", default_branch="main", branch_heads={"main": commit_id}, branch_snapshot_ids={"main": long_id("b" * 64)}, ) t.fetch_mpack.return_value = MagicMock(mpack_bytes=b"", warnings=[]) return t def _make_apply_result(): from muse.core.mpack import ApplyResult return ApplyResult( commits_written=1, snapshots_written=1, blobs_written=0, blobs_skipped=0, tags_written=0, failed_blobs=[], skipped_snapshots=[], ) class TestCloneNotice: def _clone(self, target: pathlib.Path, *, write_hooks_file: bool): from muse.core.types import long_id commit_id = long_id("a" * 64) snap_id = long_id("b" * 64) def _fake_restore(root, cid): if write_hooks_file: (root / ".musehooks.toml").write_text( '[pre-commit]\ncommands = ["echo hi"]\n', encoding="utf-8" ) with ( patch("muse.cli.commands.clone.get_signing_identity", return_value=None), patch("muse.cli.commands.clone.make_transport", return_value=_make_transport_mock()), patch("muse.cli.commands.clone.apply_mpack", return_value=_make_apply_result()), patch("muse.cli.commands.clone.set_remote"), patch("muse.cli.commands.clone.set_remote_head"), patch("muse.cli.commands.clone.set_upstream"), patch("muse.cli.commands.clone._restore_working_tree", side_effect=_fake_restore), patch("muse.cli.commands.clone.read_commit", return_value=MagicMock(snapshot_id=snap_id)), patch("muse.cli.commands.clone.read_snapshot", return_value=MagicMock(manifest={})), ): return runner.invoke( None, ["clone", "http://localhost:19999/gabriel/repo", str(target)], ) def test_clone_prints_notice_when_repo_ships_hooks(self, tmp_path: pathlib.Path) -> None: target = tmp_path / "cloned-with-hooks" r = self._clone(target, write_hooks_file=True) assert "muse hooks install" in r.stderr def test_clone_silent_when_repo_has_no_hooks(self, tmp_path: pathlib.Path) -> None: target = tmp_path / "cloned-no-hooks" r = self._clone(target, write_hooks_file=False) assert "muse hooks install" not in r.stderr