test_core_hooks.py
python
sha256:4802c0281407b5de70ce1b34bbe1d1cd055533cbd4b8be35861a6a3eb545c1dc
feat(#192): Phase 1 — .musehooks.toml parser + `muse hooks list`
Sonnet 5
patch
6 days ago
| 1 | """Tests for muse/core/hooks.py — .musehooks.toml parser and validator. |
| 2 | |
| 3 | musehub#192 Phase 1: HK_01, HK_02. |
| 4 | """ |
| 5 | |
| 6 | import pathlib |
| 7 | |
| 8 | import pytest |
| 9 | |
| 10 | from muse.core.hooks import ( |
| 11 | VALID_HOOK_POINTS, |
| 12 | HooksFile, |
| 13 | load_hooks, |
| 14 | ) |
| 15 | |
| 16 | |
| 17 | def _write_hooks(tmp_path: pathlib.Path, content: str) -> None: |
| 18 | (tmp_path / ".musehooks.toml").write_text(content, encoding="utf-8") |
| 19 | |
| 20 | |
| 21 | # --------------------------------------------------------------------------- |
| 22 | # HK_01 — parsing |
| 23 | # --------------------------------------------------------------------------- |
| 24 | |
| 25 | |
| 26 | class TestLoadHooksParsing: |
| 27 | def test_missing_file_returns_empty_hooks_file(self, tmp_path: pathlib.Path) -> None: |
| 28 | result = load_hooks(tmp_path) |
| 29 | assert result == HooksFile(hooks={}) |
| 30 | |
| 31 | def test_empty_file_returns_empty_hooks_file(self, tmp_path: pathlib.Path) -> None: |
| 32 | _write_hooks(tmp_path, "") |
| 33 | assert load_hooks(tmp_path) == HooksFile(hooks={}) |
| 34 | |
| 35 | def test_comment_only_returns_empty(self, tmp_path: pathlib.Path) -> None: |
| 36 | _write_hooks(tmp_path, "# nothing here yet\n") |
| 37 | assert load_hooks(tmp_path) == HooksFile(hooks={}) |
| 38 | |
| 39 | def test_parses_single_pre_commit_command(self, tmp_path: pathlib.Path) -> None: |
| 40 | _write_hooks( |
| 41 | tmp_path, |
| 42 | '[pre-commit]\ncommands = ["muse agent-config status --json"]\n', |
| 43 | ) |
| 44 | result = load_hooks(tmp_path) |
| 45 | assert result.hooks["pre-commit"] == ["muse agent-config status --json"] |
| 46 | |
| 47 | def test_parses_multiple_commands_preserving_order(self, tmp_path: pathlib.Path) -> None: |
| 48 | _write_hooks( |
| 49 | tmp_path, |
| 50 | '[pre-commit]\ncommands = ["first --check", "second --check", "third --check"]\n', |
| 51 | ) |
| 52 | result = load_hooks(tmp_path) |
| 53 | assert result.hooks["pre-commit"] == [ |
| 54 | "first --check", |
| 55 | "second --check", |
| 56 | "third --check", |
| 57 | ] |
| 58 | |
| 59 | def test_empty_commands_list_is_valid(self, tmp_path: pathlib.Path) -> None: |
| 60 | _write_hooks(tmp_path, "[pre-commit]\ncommands = []\n") |
| 61 | result = load_hooks(tmp_path) |
| 62 | assert result.hooks["pre-commit"] == [] |
| 63 | |
| 64 | def test_section_with_no_commands_key_is_valid(self, tmp_path: pathlib.Path) -> None: |
| 65 | _write_hooks(tmp_path, "[pre-commit]\n") |
| 66 | result = load_hooks(tmp_path) |
| 67 | assert result.hooks["pre-commit"] == [] |
| 68 | |
| 69 | |
| 70 | # --------------------------------------------------------------------------- |
| 71 | # HK_02 — malformed / invalid content handling |
| 72 | # --------------------------------------------------------------------------- |
| 73 | |
| 74 | |
| 75 | class TestLoadHooksValidation: |
| 76 | def test_malformed_toml_raises_value_error(self, tmp_path: pathlib.Path) -> None: |
| 77 | _write_hooks(tmp_path, "[pre-commit\ncommands = [") |
| 78 | with pytest.raises(ValueError, match="TOML parse error"): |
| 79 | load_hooks(tmp_path) |
| 80 | |
| 81 | def test_unknown_hook_point_raises_value_error(self, tmp_path: pathlib.Path) -> None: |
| 82 | _write_hooks(tmp_path, '[post-merge-explosion]\ncommands = ["echo hi"]\n') |
| 83 | with pytest.raises(ValueError, match="unknown hook point"): |
| 84 | load_hooks(tmp_path) |
| 85 | |
| 86 | def test_non_string_command_raises_value_error(self, tmp_path: pathlib.Path) -> None: |
| 87 | _write_hooks(tmp_path, "[pre-commit]\ncommands = [123]\n") |
| 88 | with pytest.raises(ValueError, match="must be a string"): |
| 89 | load_hooks(tmp_path) |
| 90 | |
| 91 | def test_commands_not_a_list_raises_value_error(self, tmp_path: pathlib.Path) -> None: |
| 92 | _write_hooks(tmp_path, '[pre-commit]\ncommands = "not-a-list"\n') |
| 93 | with pytest.raises(ValueError, match="must be a list"): |
| 94 | load_hooks(tmp_path) |
| 95 | |
| 96 | def test_oversized_file_raises_value_error(self, tmp_path: pathlib.Path) -> None: |
| 97 | # 1 MiB cap, matching .museattributes's own hardening precedent. |
| 98 | huge = "# " + ("x" * (1024 * 1024 + 1)) |
| 99 | _write_hooks(tmp_path, huge) |
| 100 | with pytest.raises(ValueError, match="too large"): |
| 101 | load_hooks(tmp_path) |
| 102 | |
| 103 | def test_valid_hook_points_constant_matches_mvp_scope(self) -> None: |
| 104 | # MVP is explicitly scoped to pre-commit only (musehub#192 "Out of Scope"). |
| 105 | assert VALID_HOOK_POINTS == frozenset({"pre-commit"}) |
File History
1 commit
sha256:4802c0281407b5de70ce1b34bbe1d1cd055533cbd4b8be35861a6a3eb545c1dc
feat(#192): Phase 1 — .musehooks.toml parser + `muse hooks list`
Sonnet 5
patch
6 days ago