test_core_hooks_install.py
python
sha256:d4df453a03f8b54af479f32103d9023556b28a0e38292215446959f238f3e418
feat(#192): Phase 2 — muse hooks install/uninstall/status (…
Sonnet 5
patch
11 hours ago
| 1 | """Tests for muse/core/hooks.py — local activation (musehub#192 Phase 2: HK_10-13). |
| 2 | |
| 3 | Covers install_hooks / uninstall_hooks / get_status. Installed state lives in |
| 4 | .muse/hooks-installed.toml (local only, never shared via clone) — distinct |
| 5 | from .musehooks.toml (tracked, shared) which is covered by test_core_hooks.py. |
| 6 | """ |
| 7 | |
| 8 | import pathlib |
| 9 | |
| 10 | from muse.core.hooks import ( |
| 11 | get_status, |
| 12 | install_hooks, |
| 13 | is_installed, |
| 14 | uninstall_hooks, |
| 15 | ) |
| 16 | from muse.core.paths import hooks_installed_toml_path, muse_dir |
| 17 | |
| 18 | |
| 19 | def _setup_repo_with_hooks(tmp_path: pathlib.Path, content: str | None = None) -> None: |
| 20 | muse_dir(tmp_path).mkdir(parents=True, exist_ok=True) |
| 21 | if content is not None: |
| 22 | (tmp_path / ".musehooks.toml").write_text(content, encoding="utf-8") |
| 23 | |
| 24 | |
| 25 | # --------------------------------------------------------------------------- |
| 26 | # HK_10 — install_hooks |
| 27 | # --------------------------------------------------------------------------- |
| 28 | |
| 29 | |
| 30 | class TestInstallHooks: |
| 31 | def test_install_writes_local_marker(self, tmp_path: pathlib.Path) -> None: |
| 32 | _setup_repo_with_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') |
| 33 | install_hooks(tmp_path) |
| 34 | assert hooks_installed_toml_path(tmp_path).exists() |
| 35 | |
| 36 | def test_install_makes_is_installed_true(self, tmp_path: pathlib.Path) -> None: |
| 37 | _setup_repo_with_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') |
| 38 | assert is_installed(tmp_path) is False |
| 39 | install_hooks(tmp_path) |
| 40 | assert is_installed(tmp_path) is True |
| 41 | |
| 42 | def test_install_without_musehooks_file_still_succeeds(self, tmp_path: pathlib.Path) -> None: |
| 43 | # Installing with nothing defined yet is not an error — it just means |
| 44 | # there's nothing to run until .musehooks.toml is added later. |
| 45 | _setup_repo_with_hooks(tmp_path, content=None) |
| 46 | install_hooks(tmp_path) |
| 47 | assert is_installed(tmp_path) is True |
| 48 | |
| 49 | |
| 50 | # --------------------------------------------------------------------------- |
| 51 | # HK_11 — uninstall_hooks |
| 52 | # --------------------------------------------------------------------------- |
| 53 | |
| 54 | |
| 55 | class TestUninstallHooks: |
| 56 | def test_uninstall_clears_marker(self, tmp_path: pathlib.Path) -> None: |
| 57 | _setup_repo_with_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') |
| 58 | install_hooks(tmp_path) |
| 59 | uninstall_hooks(tmp_path) |
| 60 | assert is_installed(tmp_path) is False |
| 61 | assert not hooks_installed_toml_path(tmp_path).exists() |
| 62 | |
| 63 | def test_uninstall_when_never_installed_is_a_no_op(self, tmp_path: pathlib.Path) -> None: |
| 64 | _setup_repo_with_hooks(tmp_path, content=None) |
| 65 | # Must not raise. |
| 66 | uninstall_hooks(tmp_path) |
| 67 | assert is_installed(tmp_path) is False |
| 68 | |
| 69 | |
| 70 | # --------------------------------------------------------------------------- |
| 71 | # HK_12 — get_status (three states) |
| 72 | # --------------------------------------------------------------------------- |
| 73 | |
| 74 | |
| 75 | class TestGetStatus: |
| 76 | def test_not_defined_when_no_musehooks_file(self, tmp_path: pathlib.Path) -> None: |
| 77 | _setup_repo_with_hooks(tmp_path, content=None) |
| 78 | status = get_status(tmp_path) |
| 79 | assert status.state == "not_defined" |
| 80 | |
| 81 | def test_not_defined_when_musehooks_file_has_no_commands(self, tmp_path: pathlib.Path) -> None: |
| 82 | _setup_repo_with_hooks(tmp_path, "[pre-commit]\n") |
| 83 | status = get_status(tmp_path) |
| 84 | # An empty commands list still counts as "defined" (the hook point is |
| 85 | # declared) — this exercises the boundary distinct from no file at all. |
| 86 | assert status.state == "defined_not_installed" |
| 87 | |
| 88 | def test_defined_not_installed(self, tmp_path: pathlib.Path) -> None: |
| 89 | _setup_repo_with_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') |
| 90 | status = get_status(tmp_path) |
| 91 | assert status.state == "defined_not_installed" |
| 92 | |
| 93 | def test_installed(self, tmp_path: pathlib.Path) -> None: |
| 94 | _setup_repo_with_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') |
| 95 | install_hooks(tmp_path) |
| 96 | status = get_status(tmp_path) |
| 97 | assert status.state == "installed" |
| 98 | |
| 99 | def test_not_defined_wins_even_if_marker_present(self, tmp_path: pathlib.Path) -> None: |
| 100 | # Edge case: hooks were installed, then .musehooks.toml was deleted. |
| 101 | # Nothing left to run, regardless of stale local marker. |
| 102 | _setup_repo_with_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') |
| 103 | install_hooks(tmp_path) |
| 104 | (tmp_path / ".musehooks.toml").unlink() |
| 105 | status = get_status(tmp_path) |
| 106 | assert status.state == "not_defined" |
| 107 | |
| 108 | def test_status_exposes_parsed_hooks_file(self, tmp_path: pathlib.Path) -> None: |
| 109 | _setup_repo_with_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') |
| 110 | status = get_status(tmp_path) |
| 111 | assert status.hooks_file.hooks["pre-commit"] == ["echo hi"] |
| 112 | |
| 113 | |
| 114 | # --------------------------------------------------------------------------- |
| 115 | # HK_13 — idempotency |
| 116 | # --------------------------------------------------------------------------- |
| 117 | |
| 118 | |
| 119 | class TestIdempotency: |
| 120 | def test_install_twice_is_a_no_op_not_an_error(self, tmp_path: pathlib.Path) -> None: |
| 121 | _setup_repo_with_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') |
| 122 | install_hooks(tmp_path) |
| 123 | install_hooks(tmp_path) # must not raise |
| 124 | assert is_installed(tmp_path) is True |
| 125 | |
| 126 | def test_uninstall_twice_is_a_no_op_not_an_error(self, tmp_path: pathlib.Path) -> None: |
| 127 | _setup_repo_with_hooks(tmp_path, '[pre-commit]\ncommands = ["echo hi"]\n') |
| 128 | install_hooks(tmp_path) |
| 129 | uninstall_hooks(tmp_path) |
| 130 | uninstall_hooks(tmp_path) # must not raise |
| 131 | assert is_installed(tmp_path) is False |
File History
1 commit
sha256:d4df453a03f8b54af479f32103d9023556b28a0e38292215446959f238f3e418
feat(#192): Phase 2 — muse hooks install/uninstall/status (…
Sonnet 5
patch
11 hours ago