"""Tests for muse/core/hooks.py — .musehooks.toml parser and validator. musehub#192 Phase 1: HK_01, HK_02. """ import pathlib import pytest from muse.core.hooks import ( VALID_HOOK_POINTS, HooksFile, load_hooks, ) def _write_hooks(tmp_path: pathlib.Path, content: str) -> None: (tmp_path / ".musehooks.toml").write_text(content, encoding="utf-8") # --------------------------------------------------------------------------- # HK_01 — parsing # --------------------------------------------------------------------------- class TestLoadHooksParsing: def test_missing_file_returns_empty_hooks_file(self, tmp_path: pathlib.Path) -> None: result = load_hooks(tmp_path) assert result == HooksFile(hooks={}) def test_empty_file_returns_empty_hooks_file(self, tmp_path: pathlib.Path) -> None: _write_hooks(tmp_path, "") assert load_hooks(tmp_path) == HooksFile(hooks={}) def test_comment_only_returns_empty(self, tmp_path: pathlib.Path) -> None: _write_hooks(tmp_path, "# nothing here yet\n") assert load_hooks(tmp_path) == HooksFile(hooks={}) def test_parses_single_pre_commit_command(self, tmp_path: pathlib.Path) -> None: _write_hooks( tmp_path, '[pre-commit]\ncommands = ["muse agent-config status --json"]\n', ) result = load_hooks(tmp_path) assert result.hooks["pre-commit"] == ["muse agent-config status --json"] def test_parses_multiple_commands_preserving_order(self, tmp_path: pathlib.Path) -> None: _write_hooks( tmp_path, '[pre-commit]\ncommands = ["first --check", "second --check", "third --check"]\n', ) result = load_hooks(tmp_path) assert result.hooks["pre-commit"] == [ "first --check", "second --check", "third --check", ] def test_empty_commands_list_is_valid(self, tmp_path: pathlib.Path) -> None: _write_hooks(tmp_path, "[pre-commit]\ncommands = []\n") result = load_hooks(tmp_path) assert result.hooks["pre-commit"] == [] def test_section_with_no_commands_key_is_valid(self, tmp_path: pathlib.Path) -> None: _write_hooks(tmp_path, "[pre-commit]\n") result = load_hooks(tmp_path) assert result.hooks["pre-commit"] == [] # --------------------------------------------------------------------------- # HK_02 — malformed / invalid content handling # --------------------------------------------------------------------------- class TestLoadHooksValidation: def test_malformed_toml_raises_value_error(self, tmp_path: pathlib.Path) -> None: _write_hooks(tmp_path, "[pre-commit\ncommands = [") with pytest.raises(ValueError, match="TOML parse error"): load_hooks(tmp_path) def test_unknown_hook_point_raises_value_error(self, tmp_path: pathlib.Path) -> None: _write_hooks(tmp_path, '[post-merge-explosion]\ncommands = ["echo hi"]\n') with pytest.raises(ValueError, match="unknown hook point"): load_hooks(tmp_path) def test_non_string_command_raises_value_error(self, tmp_path: pathlib.Path) -> None: _write_hooks(tmp_path, "[pre-commit]\ncommands = [123]\n") with pytest.raises(ValueError, match="must be a string"): load_hooks(tmp_path) def test_commands_not_a_list_raises_value_error(self, tmp_path: pathlib.Path) -> None: _write_hooks(tmp_path, '[pre-commit]\ncommands = "not-a-list"\n') with pytest.raises(ValueError, match="must be a list"): load_hooks(tmp_path) def test_oversized_file_raises_value_error(self, tmp_path: pathlib.Path) -> None: # 1 MiB cap, matching .museattributes's own hardening precedent. huge = "# " + ("x" * (1024 * 1024 + 1)) _write_hooks(tmp_path, huge) with pytest.raises(ValueError, match="too large"): load_hooks(tmp_path) def test_valid_hook_points_constant_matches_mvp_scope(self) -> None: # MVP is explicitly scoped to pre-commit only (musehub#192 "Out of Scope"). assert VALID_HOOK_POINTS == frozenset({"pre-commit"})