"""Seven-tier tests for ``muse/core/errors.py``. Tiers ----- Unit — ExitCode values/membership, exception construction, attributes. Integration — Exceptions caught by broad handlers, exit-code propagation via SystemExit. End-to-end — CLI commands surface correct exit codes for each error class. Stress — 10 000 exception instantiations, concurrent raises. Data integrity — Attribute correctness, message formatting, alias identity. Security — Hostile strings in messages (ANSI, null bytes, path traversal). Performance — Instantiation under 1 ms each. """ from __future__ import annotations import pathlib import threading import time import pytest # ────────────────────────────────────────────────────────────────────────────── # Unit — ExitCode # ────────────────────────────────────────────────────────────────────────────── class TestExitCode: def test_success_is_zero(self) -> None: from muse.core.errors import ExitCode assert ExitCode.SUCCESS == 0 def test_user_error_is_one(self) -> None: from muse.core.errors import ExitCode assert ExitCode.USER_ERROR == 1 def test_repo_not_found_is_two(self) -> None: from muse.core.errors import ExitCode assert ExitCode.REPO_NOT_FOUND == 2 def test_internal_error_is_three(self) -> None: from muse.core.errors import ExitCode assert ExitCode.INTERNAL_ERROR == 3 def test_not_found_is_four(self) -> None: from muse.core.errors import ExitCode assert ExitCode.NOT_FOUND == 4 def test_remote_error_is_five(self) -> None: from muse.core.errors import ExitCode assert ExitCode.REMOTE_ERROR == 5 def test_is_int_enum(self) -> None: import enum from muse.core.errors import ExitCode assert issubclass(ExitCode, enum.IntEnum) def test_all_seven_members_present(self) -> None: from muse.core.errors import ExitCode assert len(ExitCode) == 7 def test_comparable_to_int(self) -> None: from muse.core.errors import ExitCode assert ExitCode.USER_ERROR == 1 assert ExitCode.SUCCESS < ExitCode.USER_ERROR def test_usable_as_system_exit_code(self) -> None: from muse.core.errors import ExitCode with pytest.raises(SystemExit) as exc: raise SystemExit(ExitCode.USER_ERROR) assert exc.value.code == 1 # ────────────────────────────────────────────────────────────────────────────── # Unit — MuseCLIError # ────────────────────────────────────────────────────────────────────────────── class TestMuseCLIError: def test_is_exception(self) -> None: from muse.core.errors import MuseCLIError assert issubclass(MuseCLIError, Exception) def test_message_stored(self) -> None: from muse.core.errors import MuseCLIError e = MuseCLIError("oops") assert str(e) == "oops" def test_default_exit_code_is_internal_error(self) -> None: from muse.core.errors import ExitCode, MuseCLIError e = MuseCLIError("oops") assert e.exit_code == ExitCode.INTERNAL_ERROR def test_custom_exit_code(self) -> None: from muse.core.errors import ExitCode, MuseCLIError e = MuseCLIError("bad input", ExitCode.USER_ERROR) assert e.exit_code == ExitCode.USER_ERROR def test_catchable_as_exception(self) -> None: from muse.core.errors import MuseCLIError with pytest.raises(Exception): raise MuseCLIError("test") # ────────────────────────────────────────────────────────────────────────────── # Unit — RepoNotFoundError # ────────────────────────────────────────────────────────────────────────────── class TestRepoNotFoundError: def test_is_muse_cli_error(self) -> None: from muse.core.errors import MuseCLIError, RepoNotFoundError assert issubclass(RepoNotFoundError, MuseCLIError) def test_default_message_mentions_muse_init(self) -> None: from muse.core.errors import RepoNotFoundError e = RepoNotFoundError() assert "muse init" in str(e).lower() or "init" in str(e) def test_exit_code_is_repo_not_found(self) -> None: from muse.core.errors import ExitCode, RepoNotFoundError e = RepoNotFoundError() assert e.exit_code == ExitCode.REPO_NOT_FOUND def test_custom_message(self) -> None: from muse.core.errors import RepoNotFoundError e = RepoNotFoundError("custom msg") assert "custom msg" in str(e) def test_catchable_as_muse_cli_error(self) -> None: from muse.core.errors import MuseCLIError, RepoNotFoundError with pytest.raises(MuseCLIError): raise RepoNotFoundError() # ────────────────────────────────────────────────────────────────────────────── # Unit — MuseNotARepoError alias # ────────────────────────────────────────────────────────────────────────────── class TestMuseNotARepoError: def test_is_same_class_as_repo_not_found(self) -> None: from muse.core.errors import MuseNotARepoError, RepoNotFoundError assert MuseNotARepoError is RepoNotFoundError def test_alias_raises_same_exception(self) -> None: from muse.core.errors import MuseNotARepoError, RepoNotFoundError with pytest.raises(RepoNotFoundError): raise MuseNotARepoError() # ────────────────────────────────────────────────────────────────────────────── # Unit — UntrustedRepositoryError # ────────────────────────────────────────────────────────────────────────────── class TestUntrustedRepositoryError: def test_is_permission_error(self) -> None: from muse.core.errors import UntrustedRepositoryError assert issubclass(UntrustedRepositoryError, PermissionError) def test_stores_repo_path(self) -> None: from muse.core.errors import UntrustedRepositoryError e = UntrustedRepositoryError("/tmp/repo", owner_uid=1000, current_uid=1001) assert e.repo_path == "/tmp/repo" def test_stores_owner_uid(self) -> None: from muse.core.errors import UntrustedRepositoryError e = UntrustedRepositoryError("/tmp/repo", owner_uid=1000, current_uid=1001) assert e.owner_uid == 1000 def test_stores_current_uid(self) -> None: from muse.core.errors import UntrustedRepositoryError e = UntrustedRepositoryError("/tmp/repo", owner_uid=1000, current_uid=1001) assert e.current_uid == 1001 def test_message_mentions_path(self) -> None: from muse.core.errors import UntrustedRepositoryError e = UntrustedRepositoryError("/tmp/repo", owner_uid=1000, current_uid=1001) assert "/tmp/repo" in str(e) def test_message_mentions_both_uids(self) -> None: from muse.core.errors import UntrustedRepositoryError e = UntrustedRepositoryError("/tmp/repo", owner_uid=1000, current_uid=1001) msg = str(e) assert "1000" in msg assert "1001" in msg def test_message_mentions_trust_command(self) -> None: from muse.core.errors import UntrustedRepositoryError e = UntrustedRepositoryError("/tmp/repo", owner_uid=1000, current_uid=1001) assert "muse trust" in str(e) def test_catchable_as_permission_error(self) -> None: from muse.core.errors import UntrustedRepositoryError with pytest.raises(PermissionError): raise UntrustedRepositoryError("/tmp/repo", 1000, 1001) # ────────────────────────────────────────────────────────────────────────────── # Unit — HubFingerprintMismatchError # ────────────────────────────────────────────────────────────────────────────── class TestHubFingerprintMismatchError: def test_is_exception(self) -> None: from muse.core.errors import HubFingerprintMismatchError assert issubclass(HubFingerprintMismatchError, Exception) def test_stores_hostname(self) -> None: from muse.core.errors import HubFingerprintMismatchError e = HubFingerprintMismatchError("hub.example.com", "aaa", "bbb") assert e.hostname == "hub.example.com" def test_stores_stored_fingerprint(self) -> None: from muse.core.errors import HubFingerprintMismatchError e = HubFingerprintMismatchError("hub.example.com", "aaa", "bbb") assert e.stored_fingerprint == "aaa" def test_stores_actual_fingerprint(self) -> None: from muse.core.errors import HubFingerprintMismatchError e = HubFingerprintMismatchError("hub.example.com", "aaa", "bbb") assert e.actual_fingerprint == "bbb" def test_message_mentions_hostname(self) -> None: from muse.core.errors import HubFingerprintMismatchError e = HubFingerprintMismatchError("hub.example.com", "aaa", "bbb") assert "hub.example.com" in str(e) def test_message_mentions_both_fingerprints(self) -> None: from muse.core.errors import HubFingerprintMismatchError e = HubFingerprintMismatchError("hub.example.com", "stored-fp", "actual-fp") msg = str(e) assert "stored-fp" in msg assert "actual-fp" in msg def test_message_mentions_mitm_risk(self) -> None: from muse.core.errors import HubFingerprintMismatchError e = HubFingerprintMismatchError("hub.example.com", "aaa", "bbb") msg = str(e).lower() assert "man-in-the-middle" in msg or "mitm" in msg or "mismatch" in msg def test_message_mentions_hub_reset(self) -> None: from muse.core.errors import HubFingerprintMismatchError e = HubFingerprintMismatchError("hub.example.com", "aaa", "bbb") assert "hub-reset" in str(e) # ────────────────────────────────────────────────────────────────────────────── # Integration — exception hierarchy and handler compatibility # ────────────────────────────────────────────────────────────────────────────── class TestIntegration: def test_repo_not_found_not_caught_by_os_error(self) -> None: """MuseCLIError inherits Exception, not OSError — OSError does not catch it.""" from muse.core.errors import RepoNotFoundError with pytest.raises(RepoNotFoundError): try: raise RepoNotFoundError() except OSError: pytest.fail("RepoNotFoundError should not be caught by OSError") def test_untrusted_repo_caught_by_os_error(self) -> None: from muse.core.errors import UntrustedRepositoryError with pytest.raises(OSError): raise UntrustedRepositoryError("/p", 0, 1) def test_exit_code_propagates_through_system_exit(self) -> None: from muse.core.errors import ExitCode, MuseCLIError e = MuseCLIError("fail", ExitCode.NOT_FOUND) with pytest.raises(SystemExit) as exc: raise SystemExit(e.exit_code) assert exc.value.code == 4 def test_all_exit_codes_valid_process_exit_codes(self) -> None: from muse.core.errors import ExitCode for code in ExitCode: assert 0 <= int(code) <= 127 def test_repo_not_found_exit_code_matches_enum(self) -> None: from muse.core.errors import ExitCode, RepoNotFoundError e = RepoNotFoundError() assert int(e.exit_code) == int(ExitCode.REPO_NOT_FOUND) # ────────────────────────────────────────────────────────────────────────────── # End-to-end — CLI surfaces correct exit codes # ────────────────────────────────────────────────────────────────────────────── class TestEndToEnd: def test_muse_outside_repo_exits_nonzero(self, tmp_path: pathlib.Path) -> None: import os from tests.cli_test_helper import CliRunner r = CliRunner() saved = os.getcwd() try: os.chdir(tmp_path) result = r.invoke(None, ["status"]) finally: os.chdir(saved) assert result.exit_code != 0 def test_muse_outside_repo_exit_code_is_repo_not_found(self, tmp_path: pathlib.Path) -> None: import os from muse.core.errors import ExitCode from tests.cli_test_helper import CliRunner r = CliRunner() saved = os.getcwd() try: os.chdir(tmp_path) result = r.invoke(None, ["status"]) finally: os.chdir(saved) assert result.exit_code == ExitCode.REPO_NOT_FOUND # ────────────────────────────────────────────────────────────────────────────── # Stress # ────────────────────────────────────────────────────────────────────────────── class TestStress: def test_10000_muse_cli_error_instantiations(self) -> None: from muse.core.errors import ExitCode, MuseCLIError for i in range(10_000): e = MuseCLIError(f"error {i}", ExitCode.USER_ERROR) assert e.exit_code == ExitCode.USER_ERROR def test_concurrent_exception_raises_all_succeed(self) -> None: from muse.core.errors import RepoNotFoundError results: list[bool] = [] lock = threading.Lock() def _raise() -> None: try: raise RepoNotFoundError() except RepoNotFoundError: with lock: results.append(True) threads = [threading.Thread(target=_raise) for _ in range(50)] for t in threads: t.start() for t in threads: t.join() assert len(results) == 50 def test_10000_untrusted_repo_error_instantiations(self) -> None: from muse.core.errors import UntrustedRepositoryError for i in range(10_000): e = UntrustedRepositoryError(f"/repo/{i}", owner_uid=i, current_uid=i + 1) assert e.owner_uid == i # ────────────────────────────────────────────────────────────────────────────── # Data integrity # ────────────────────────────────────────────────────────────────────────────── class TestDataIntegrity: def test_exit_code_values_are_unique(self) -> None: from muse.core.errors import ExitCode values = [int(c) for c in ExitCode] assert len(values) == len(set(values)) def test_muse_cli_error_exit_code_attribute_is_exit_code_instance(self) -> None: from muse.core.errors import ExitCode, MuseCLIError e = MuseCLIError("x", ExitCode.REMOTE_ERROR) assert isinstance(e.exit_code, ExitCode) def test_untrusted_repo_attributes_independent_of_message(self) -> None: from muse.core.errors import UntrustedRepositoryError e = UntrustedRepositoryError("/path", owner_uid=42, current_uid=99) assert e.repo_path == "/path" assert e.owner_uid == 42 assert e.current_uid == 99 def test_fingerprint_mismatch_attributes_independent_of_message(self) -> None: from muse.core.errors import HubFingerprintMismatchError e = HubFingerprintMismatchError("host", "s1", "a1") assert e.hostname == "host" assert e.stored_fingerprint == "s1" assert e.actual_fingerprint == "a1" def test_repo_not_found_is_exact_alias(self) -> None: from muse.core.errors import MuseNotARepoError, RepoNotFoundError assert MuseNotARepoError is RepoNotFoundError assert id(MuseNotARepoError) == id(RepoNotFoundError) # ────────────────────────────────────────────────────────────────────────────── # Security # ────────────────────────────────────────────────────────────────────────────── class TestSecurity: def test_ansi_in_untrusted_path_preserved_in_attribute(self) -> None: """The path attribute stores raw input — callers must sanitize for display.""" from muse.core.errors import UntrustedRepositoryError malicious = "/tmp/\x1b[31mmalicious\x1b[0m" e = UntrustedRepositoryError(malicious, owner_uid=0, current_uid=1) assert e.repo_path == malicious # stored as-is def test_null_byte_in_muse_cli_error_message_does_not_crash(self) -> None: from muse.core.errors import MuseCLIError e = MuseCLIError("msg\x00with\x00nulls") assert "\x00" in str(e) # stored, not stripped def test_very_long_message_does_not_crash(self) -> None: from muse.core.errors import MuseCLIError long_msg = "x" * 100_000 e = MuseCLIError(long_msg) assert len(str(e)) == 100_000 def test_fingerprint_mismatch_with_hostile_fingerprint_strings(self) -> None: from muse.core.errors import HubFingerprintMismatchError malicious_fp = "'; DROP TABLE fingerprints; --" e = HubFingerprintMismatchError("host", malicious_fp, "actual") assert e.stored_fingerprint == malicious_fp # ────────────────────────────────────────────────────────────────────────────── # Performance # ────────────────────────────────────────────────────────────────────────────── class TestPerformance: def test_exit_code_lookup_under_1ms(self) -> None: from muse.core.errors import ExitCode start = time.perf_counter() for _ in range(1000): _ = ExitCode.USER_ERROR elapsed = time.perf_counter() - start assert elapsed < 0.1 # 1000 lookups in < 100 ms def test_muse_cli_error_instantiation_under_1ms_each(self) -> None: from muse.core.errors import ExitCode, MuseCLIError start = time.perf_counter() for i in range(1000): MuseCLIError(f"msg {i}", ExitCode.USER_ERROR) elapsed = time.perf_counter() - start assert elapsed < 1.0 # 1000 instances in < 1s (i.e. < 1ms each) def test_untrusted_repo_error_instantiation_fast(self) -> None: from muse.core.errors import UntrustedRepositoryError start = time.perf_counter() for i in range(1000): UntrustedRepositoryError(f"/repo/{i}", i, i + 1) elapsed = time.perf_counter() - start assert elapsed < 1.0