"""Shared test fixtures for mcadb tests.""" from dataclasses import dataclass, field from typing import Any from unittest.mock import AsyncMock import pytest from src.models import CommandResult from src.server import ADBServer # --- Helpers --- def ok(stdout: str = "", stderr: str = "") -> CommandResult: """Create a successful CommandResult.""" return CommandResult(success=True, stdout=stdout, stderr=stderr, returncode=0) def fail(stderr: str = "error", stdout: str = "") -> CommandResult: """Create a failed CommandResult.""" return CommandResult(success=False, stdout=stdout, stderr=stderr, returncode=1) # --- Mock Context --- @dataclass class ElicitResult: """Minimal stand-in for FastMCP's ElicitationResult.""" action: str = "accept" content: str = "" @dataclass class MockContext: """Mock MCP Context that records calls for assertion.""" messages: list[tuple[str, str]] = field(default_factory=list) _elicit_response: ElicitResult = field(default_factory=ElicitResult) async def info(self, msg: str) -> None: self.messages.append(("info", msg)) async def warning(self, msg: str) -> None: self.messages.append(("warning", msg)) async def error(self, msg: str) -> None: self.messages.append(("error", msg)) async def elicit(self, msg: str, options: list[str] | None = None) -> ElicitResult: self.messages.append(("elicit", msg)) return self._elicit_response def set_elicit(self, action: str = "accept", content: str = "") -> None: """Configure the next elicit response.""" self._elicit_response = ElicitResult(action=action, content=content) # --- Fixtures --- def _reset_config(monkeypatch: pytest.MonkeyPatch, config_dir: Any) -> None: """Reset the Config singleton and point it at a temp directory. CONFIG_DIR and CONFIG_FILE are module-level variables computed at import time, so setting the env var isn't enough — we must patch the variables directly. """ from pathlib import Path config_path = Path(config_dir) monkeypatch.setattr("src.config.Config._instance", None) monkeypatch.setattr("src.config.CONFIG_DIR", config_path) monkeypatch.setattr("src.config.CONFIG_FILE", config_path / "config.json") @pytest.fixture def server() -> ADBServer: """Create an ADBServer with mocked ADB execution. Both run_adb and run_shell_args are replaced with AsyncMock, so no real subprocess calls are made. Configure return values per-test with server.run_adb.return_value = ok("..."). """ s = ADBServer() s.run_adb = AsyncMock(return_value=ok()) # type: ignore[method-assign] s.run_shell_args = AsyncMock(return_value=ok()) # type: ignore[method-assign] s.run_shell = AsyncMock(return_value=ok()) # type: ignore[method-assign] s.get_device_property = AsyncMock(return_value=None) # type: ignore[method-assign] return s @pytest.fixture def ctx() -> MockContext: """Create a mock MCP Context.""" return MockContext() @pytest.fixture def _dev_mode(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None: """Enable developer mode for the test.""" _reset_config(monkeypatch, tmp_path / "dev-config") from src.config import get_config config = get_config() config._settings["developer_mode"] = True @pytest.fixture def _no_dev_mode(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None: """Disable developer mode for the test.""" _reset_config(monkeypatch, tmp_path / "nodev-config") from src.config import get_config config = get_config() config._settings["developer_mode"] = False @pytest.fixture(autouse=True) def _isolate_config(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None: """Isolate config to a temp directory so tests don't touch real config.""" _reset_config(monkeypatch, tmp_path / "config")