"""Pytest configuration and shared fixtures for the muse test suite. All fixtures defined here are automatically available to every test module in the ``tests/`` directory without explicit import. """ from __future__ import annotations import os import pathlib from typing import Generator from unittest.mock import patch import pytest @pytest.fixture(autouse=True) def _isolate_muse_home(tmp_path: pathlib.Path) -> Generator[None, None, None]: """Redirect every module-level constant that caches ``~/.muse/`` to a throwaway temp directory for the duration of each test, and isolate the OS keychain so tests never read or write gabriel's real mnemonic. Without this, any test that exercises auth, identity, keychain, hub-trust, or global config would read from *and write to* the real ``~/.muse/``, silently clobbering the developer's Ed25519 identity and invalidating push authentication against the local hub. Constants patched because they are computed once at import time: muse.core.keypair _KEYS_DIR muse.core.identity _IDENTITY_DIR, _IDENTITY_FILE muse.core.hub_trust _HUB_TRUST_FILE muse.cli.config _GLOBAL_MUSE_DIR, _GLOBAL_CONFIG_FILE muse.core.agent_slots _SLOTS_FILE, _SLOTS_DIR Keychain functions patched to an in-memory store so no test can read or overwrite the real OS keychain mnemonic: muse.core.keychain is_available, store, load, delete All tests that previously called a per-file ``_patch_home()`` helper continue to work — their monkeypatches win over these because they run *after* this fixture sets up the initial redirect. """ from muse.core.paths import muse_dir fake_home = tmp_path / "fake_home" fake_home.mkdir(parents=True, exist_ok=True) fake_muse = muse_dir(fake_home) fake_muse.mkdir(parents=True, exist_ok=True) import muse.cli.config as _cfg_mod import muse.core.agent_slots as _slots_mod import muse.core.hub_trust as _ht_mod import muse.core.identity as _id_mod import muse.core.keypair as _kp_mod import muse.core.keychain as _kc_mod # Fresh in-memory keychain for every test — never touches the OS keychain. _kc_store: dict[str, str] = {} with ( patch.object(pathlib.Path, "home", staticmethod(lambda: fake_home)), patch.object(_kp_mod, "_KEYS_DIR", fake_muse / "keys"), patch.object(_id_mod, "_IDENTITY_DIR", fake_muse), patch.object(_id_mod, "_IDENTITY_FILE", fake_muse / "identity.toml"), patch.object(_ht_mod, "_HUB_TRUST_FILE", fake_muse / "hub_trust.toml"), patch.object(_cfg_mod, "_GLOBAL_MUSE_DIR", fake_muse), patch.object(_cfg_mod, "_GLOBAL_CONFIG_FILE", fake_muse / "config.toml"), patch.object(_slots_mod, "_SLOTS_FILE", fake_muse / "agent-slots.toml"), patch.object(_slots_mod, "_SLOTS_DIR", fake_muse), patch.object(_kc_mod, "is_available", lambda: True), patch.object(_kc_mod, "store", lambda m: _kc_store.__setitem__("mnemonic", m) or True), patch.object(_kc_mod, "load", lambda: _kc_store.get("mnemonic")), patch.object(_kc_mod, "delete", lambda: _kc_store.pop("mnemonic", None) is not None), ): yield @pytest.fixture(autouse=True) def _restore_cwd() -> Generator[None, None, None]: """Restore the working directory and MUSE_REPO_ROOT after every test. Several tests call ``os.chdir()`` to enter a temporary repository, and some directly mutate ``os.environ["MUSE_REPO_ROOT"]`` without cleanup. Without this fixture those side-effects leak into subsequent tests, causing ``find_repo_root()`` to resolve against a stale path and producing spurious failures across the entire suite. Note: module-scoped fixtures that set these values run *before* this function-scoped fixture captures them, so they must manage their own cleanup (yield + restore in the fixture body). """ original_cwd = os.getcwd() original_root = os.environ.get("MUSE_REPO_ROOT") yield os.chdir(original_cwd) if original_root is None: os.environ.pop("MUSE_REPO_ROOT", None) else: os.environ["MUSE_REPO_ROOT"] = original_root @pytest.fixture() def muse_repo(tmp_path: pathlib.Path) -> pathlib.Path: """Fully initialised Muse repo in an isolated temp directory. Runs ``muse init`` via the CLI so HEAD, repo.json, .museignore, and .museattributes are all present — identical to what a real user sees after ``muse init``. Use this fixture whenever you need a complete repo (config, commit, checkout, push dry-runs, etc.). The repo root is returned. To run CLI commands against it, pass ``cwd=muse_repo`` to ``runner.invoke`` **or** set ``env={"MUSE_REPO_ROOT": str(muse_repo)}``. """ from tests.cli_test_helper import CliRunner CliRunner().invoke(None, ["init"], cwd=tmp_path) return tmp_path @pytest.fixture() def bare_muse_repo(tmp_path: pathlib.Path) -> pathlib.Path: """Minimal ``.muse/`` directory tree in an isolated temp directory. Calls :func:`muse.core.paths.init_repo_dirs` — creates the canonical subdirectory layout without writing HEAD, repo.json, or any other file. Use this for low-level unit tests that construct objects directly (commit records, snapshots, bridge state) and don't need a full CLI-initialised repo. The repo root is returned. """ from muse.core.paths import init_repo_dirs return init_repo_dirs(tmp_path)