conftest.py
python
sha256:d11a87833d5fad6059b7662844bf5448a8911a17cce7a51811f71ad394f248eb
bump to v0.2.0rc13
Human
patch
6 days ago
| 1 | """Pytest configuration and shared fixtures for the muse test suite. |
| 2 | |
| 3 | All fixtures defined here are automatically available to every test module |
| 4 | in the ``tests/`` directory without explicit import. |
| 5 | """ |
| 6 | |
| 7 | from __future__ import annotations |
| 8 | |
| 9 | import os |
| 10 | import pathlib |
| 11 | from typing import Generator |
| 12 | from unittest.mock import patch |
| 13 | |
| 14 | import pytest |
| 15 | |
| 16 | |
| 17 | @pytest.fixture(autouse=True) |
| 18 | def _isolate_muse_home(tmp_path: pathlib.Path) -> Generator[None, None, None]: |
| 19 | """Redirect every module-level constant that caches ``~/.muse/`` to a |
| 20 | throwaway temp directory for the duration of each test, and isolate the |
| 21 | OS keychain so tests never read or write gabriel's real mnemonic. |
| 22 | |
| 23 | Without this, any test that exercises auth, identity, keychain, hub-trust, |
| 24 | or global config would read from *and write to* the real ``~/.muse/``, |
| 25 | silently clobbering the developer's Ed25519 identity and invalidating |
| 26 | push authentication against the local hub. |
| 27 | |
| 28 | Constants patched because they are computed once at import time: |
| 29 | |
| 30 | muse.core.keypair _KEYS_DIR |
| 31 | muse.core.identity _IDENTITY_DIR, _IDENTITY_FILE |
| 32 | muse.core.hub_trust _HUB_TRUST_FILE |
| 33 | muse.cli.config _GLOBAL_MUSE_DIR, _GLOBAL_CONFIG_FILE |
| 34 | muse.core.agent_slots _SLOTS_FILE, _SLOTS_DIR |
| 35 | |
| 36 | Keychain functions patched to an in-memory store so no test can read or |
| 37 | overwrite the real OS keychain mnemonic: |
| 38 | |
| 39 | muse.core.keychain is_available, store, load, delete |
| 40 | |
| 41 | All tests that previously called a per-file ``_patch_home()`` helper |
| 42 | continue to work — their monkeypatches win over these because they run |
| 43 | *after* this fixture sets up the initial redirect. |
| 44 | """ |
| 45 | from muse.core.paths import muse_dir |
| 46 | |
| 47 | fake_home = tmp_path / "fake_home" |
| 48 | fake_home.mkdir(parents=True, exist_ok=True) |
| 49 | fake_muse = muse_dir(fake_home) |
| 50 | fake_muse.mkdir(parents=True, exist_ok=True) |
| 51 | |
| 52 | import muse.cli.config as _cfg_mod |
| 53 | import muse.core.agent_slots as _slots_mod |
| 54 | import muse.core.hub_trust as _ht_mod |
| 55 | import muse.core.identity as _id_mod |
| 56 | import muse.core.keypair as _kp_mod |
| 57 | import muse.core.keychain as _kc_mod |
| 58 | |
| 59 | # Fresh in-memory keychain for every test — never touches the OS keychain. |
| 60 | _kc_store: dict[str, str] = {} |
| 61 | |
| 62 | with ( |
| 63 | patch.object(pathlib.Path, "home", staticmethod(lambda: fake_home)), |
| 64 | patch.object(_kp_mod, "_KEYS_DIR", fake_muse / "keys"), |
| 65 | patch.object(_id_mod, "_IDENTITY_DIR", fake_muse), |
| 66 | patch.object(_id_mod, "_IDENTITY_FILE", fake_muse / "identity.toml"), |
| 67 | patch.object(_ht_mod, "_HUB_TRUST_FILE", fake_muse / "hub_trust.toml"), |
| 68 | patch.object(_cfg_mod, "_GLOBAL_MUSE_DIR", fake_muse), |
| 69 | patch.object(_cfg_mod, "_GLOBAL_CONFIG_FILE", fake_muse / "config.toml"), |
| 70 | patch.object(_slots_mod, "_SLOTS_FILE", fake_muse / "agent-slots.toml"), |
| 71 | patch.object(_slots_mod, "_SLOTS_DIR", fake_muse), |
| 72 | patch.object(_kc_mod, "is_available", lambda: True), |
| 73 | patch.object(_kc_mod, "store", lambda m: _kc_store.__setitem__("mnemonic", m) or True), |
| 74 | patch.object(_kc_mod, "load", lambda: _kc_store.get("mnemonic")), |
| 75 | patch.object(_kc_mod, "delete", lambda: _kc_store.pop("mnemonic", None) is not None), |
| 76 | ): |
| 77 | yield |
| 78 | |
| 79 | |
| 80 | @pytest.fixture(autouse=True) |
| 81 | def _restore_cwd() -> Generator[None, None, None]: |
| 82 | """Restore the working directory and MUSE_REPO_ROOT after every test. |
| 83 | |
| 84 | Several tests call ``os.chdir()`` to enter a temporary repository, and |
| 85 | some directly mutate ``os.environ["MUSE_REPO_ROOT"]`` without cleanup. |
| 86 | Without this fixture those side-effects leak into subsequent tests, |
| 87 | causing ``find_repo_root()`` to resolve against a stale path and |
| 88 | producing spurious failures across the entire suite. |
| 89 | |
| 90 | Note: module-scoped fixtures that set these values run *before* this |
| 91 | function-scoped fixture captures them, so they must manage their own |
| 92 | cleanup (yield + restore in the fixture body). |
| 93 | """ |
| 94 | original_cwd = os.getcwd() |
| 95 | original_root = os.environ.get("MUSE_REPO_ROOT") |
| 96 | yield |
| 97 | os.chdir(original_cwd) |
| 98 | if original_root is None: |
| 99 | os.environ.pop("MUSE_REPO_ROOT", None) |
| 100 | else: |
| 101 | os.environ["MUSE_REPO_ROOT"] = original_root |
| 102 | |
| 103 | |
| 104 | @pytest.fixture() |
| 105 | def muse_repo(tmp_path: pathlib.Path) -> pathlib.Path: |
| 106 | """Fully initialised Muse repo in an isolated temp directory. |
| 107 | |
| 108 | Runs ``muse init`` via the CLI so HEAD, repo.json, .museignore, and |
| 109 | .museattributes are all present — identical to what a real user sees after |
| 110 | ``muse init``. Use this fixture whenever you need a complete repo |
| 111 | (config, commit, checkout, push dry-runs, etc.). |
| 112 | |
| 113 | The repo root is returned. To run CLI commands against it, pass |
| 114 | ``cwd=muse_repo`` to ``runner.invoke`` **or** set |
| 115 | ``env={"MUSE_REPO_ROOT": str(muse_repo)}``. |
| 116 | """ |
| 117 | from tests.cli_test_helper import CliRunner |
| 118 | CliRunner().invoke(None, ["init"], cwd=tmp_path) |
| 119 | return tmp_path |
| 120 | |
| 121 | |
| 122 | @pytest.fixture() |
| 123 | def bare_muse_repo(tmp_path: pathlib.Path) -> pathlib.Path: |
| 124 | """Minimal ``.muse/`` directory tree in an isolated temp directory. |
| 125 | |
| 126 | Calls :func:`muse.core.paths.init_repo_dirs` — creates the canonical |
| 127 | subdirectory layout without writing HEAD, repo.json, or any other file. |
| 128 | Use this for low-level unit tests that construct objects directly (commit |
| 129 | records, snapshots, bridge state) and don't need a full CLI-initialised |
| 130 | repo. |
| 131 | |
| 132 | The repo root is returned. |
| 133 | """ |
| 134 | from muse.core.paths import init_repo_dirs |
| 135 | return init_repo_dirs(tmp_path) |
File History
1 commit
sha256:d11a87833d5fad6059b7662844bf5448a8911a17cce7a51811f71ad394f248eb
bump to v0.2.0rc13
Human
patch
6 days ago