"""Unit tests for mydeepagent doctor — 8-check full diagnostic suite.""" from __future__ import annotations import shutil import subprocess import sys from pathlib import Path from unittest.mock import AsyncMock, MagicMock import httpx import pytest from my_deepagent.cli.doctor import ( _check_config_and_governance, _check_disk_and_db, _check_git, _check_openrouter_api_key, _check_openrouter_ping_and_upsert, _check_python, _check_uv, _check_workspace, ) from my_deepagent.errors import MyDeepAgentError # --------------------------------------------------------------------------- # 1. _check_python # --------------------------------------------------------------------------- def test_check_python_ok_in_312(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "version_info", (3, 12, 0, "final", 0)) monkeypatch.setattr(sys, "version", "3.12.0 (default, ...)") result = _check_python() assert result.status == "ok" assert result.name == "python" assert "3.12.0" in result.detail def test_check_python_ok_in_313(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "version_info", (3, 13, 0, "final", 0)) monkeypatch.setattr(sys, "version", "3.13.0 (default, ...)") result = _check_python() assert result.status == "ok" def test_check_python_fail_in_310(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "version_info", (3, 10, 0, "final", 0)) monkeypatch.setattr(sys, "version", "3.10.0 (default, ...)") result = _check_python() assert result.status == "fail" assert "3.10.0" in result.detail def test_check_python_fail_in_314(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "version_info", (3, 14, 0, "alpha", 0)) monkeypatch.setattr(sys, "version", "3.14.0a1 (default, ...)") result = _check_python() assert result.status == "fail" # --------------------------------------------------------------------------- # 2. _check_uv # --------------------------------------------------------------------------- def test_check_uv_warn_when_missing(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(shutil, "which", lambda _: None) result = _check_uv() assert result.status == "warn" assert "not on PATH" in result.detail def test_check_uv_ok_when_present(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(shutil, "which", lambda _: "/usr/local/bin/uv") fake_run = MagicMock() fake_run.return_value.stdout = "uv 0.5.0" monkeypatch.setattr(subprocess, "run", fake_run) result = _check_uv() assert result.status == "ok" assert "uv 0.5.0" in result.detail def test_check_uv_warn_on_timeout(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(shutil, "which", lambda _: "/usr/local/bin/uv") monkeypatch.setattr( subprocess, "run", MagicMock(side_effect=subprocess.TimeoutExpired(["uv"], 5)), ) result = _check_uv() assert result.status == "warn" assert "version probe failed" in result.detail # --------------------------------------------------------------------------- # 3. _check_git # --------------------------------------------------------------------------- def test_check_git_warn_when_missing(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(shutil, "which", lambda _: None) result = _check_git() assert result.status == "warn" assert "not on PATH" in result.detail def test_check_git_ok_when_present(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(shutil, "which", lambda _: "/usr/bin/git") fake_run = MagicMock() fake_run.return_value.stdout = "git version 2.40.0" monkeypatch.setattr(subprocess, "run", fake_run) result = _check_git() assert result.status == "ok" assert "2.40.0" in result.detail # --------------------------------------------------------------------------- # 4. _check_workspace # --------------------------------------------------------------------------- def test_check_workspace_ok_when_writable(tmp_path: Path) -> None: cfg = MagicMock() cfg.workspace_root = tmp_path result = _check_workspace(cfg) assert result.status == "ok" assert str(tmp_path) in result.detail def test_check_workspace_creates_if_missing(tmp_path: Path) -> None: new_dir = tmp_path / "new_workspace" cfg = MagicMock() cfg.workspace_root = new_dir result = _check_workspace(cfg) assert result.status == "ok" assert new_dir.exists() def test_check_workspace_fail_if_not_writable( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: cfg = MagicMock() cfg.workspace_root = tmp_path def _raise_oserror(self: object, data: str, **kwargs: object) -> None: raise OSError("read-only filesystem") monkeypatch.setattr(Path, "write_text", _raise_oserror) result = _check_workspace(cfg) assert result.status == "fail" assert "not writable" in result.detail # --------------------------------------------------------------------------- # 5. _check_config_and_governance # --------------------------------------------------------------------------- def test_check_governance_fail_without_consent(monkeypatch: pytest.MonkeyPatch) -> None: import my_deepagent.cli.doctor as doctor_module monkeypatch.setattr(doctor_module, "has_consent", lambda _: False) cfg = MagicMock() result = _check_config_and_governance(cfg) assert result.status == "fail" assert "mydeepagent init" in result.detail def test_check_governance_ok_with_consent(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: import my_deepagent.cli.doctor as doctor_module monkeypatch.setattr(doctor_module, "has_consent", lambda _: True) cfg = MagicMock() cfg.data_dir = tmp_path result = _check_config_and_governance(cfg) assert result.status == "ok" assert str(tmp_path) in result.detail # --------------------------------------------------------------------------- # 6. _check_openrouter_api_key # --------------------------------------------------------------------------- def test_check_openrouter_api_key_ok(monkeypatch: pytest.MonkeyPatch) -> None: import my_deepagent.cli.doctor as doctor_module api_key = "sk-or-test-1234" monkeypatch.setattr(doctor_module, "resolve_openrouter_api_key", lambda cfg: api_key) cfg = MagicMock() result = _check_openrouter_api_key(cfg) assert result.status == "ok" assert str(len(api_key)) in result.detail # "15 chars" def test_check_openrouter_api_key_fail(monkeypatch: pytest.MonkeyPatch) -> None: import my_deepagent.cli.doctor as doctor_module def _raise(cfg: object) -> str: raise MyDeepAgentError.human_required( "backend_auth_failed", message="missing", recovery_hint="run login", ) monkeypatch.setattr(doctor_module, "resolve_openrouter_api_key", _raise) cfg = MagicMock() result = _check_openrouter_api_key(cfg) assert result.status == "fail" assert "run login" in result.detail # --------------------------------------------------------------------------- # 7. _check_openrouter_ping_and_upsert (async) # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_check_openrouter_ping_warn_no_key(monkeypatch: pytest.MonkeyPatch) -> None: import my_deepagent.cli.doctor as doctor_module def _raise(cfg: object) -> str: raise MyDeepAgentError.human_required("backend_auth_failed", message="missing") monkeypatch.setattr(doctor_module, "resolve_openrouter_api_key", _raise) cfg = MagicMock() result = await _check_openrouter_ping_and_upsert(cfg) assert result.status == "warn" assert "skipped" in result.detail @pytest.mark.asyncio async def test_check_openrouter_ping_ok(monkeypatch: pytest.MonkeyPatch) -> None: import my_deepagent.cli.doctor as doctor_module from my_deepagent.monitoring.pricing import ModelPrice monkeypatch.setattr(doctor_module, "resolve_openrouter_api_key", lambda cfg: "sk-test") fake_prices = [ ModelPrice("model/a", 1.0, 2.0, 4096), ModelPrice("model/b", 0.5, 1.0, 8192), ] monkeypatch.setattr( doctor_module, "fetch_openrouter_pricing", AsyncMock(return_value=fake_prices), ) monkeypatch.setattr(doctor_module, "_upsert_pricing", AsyncMock()) cfg = MagicMock() result = await _check_openrouter_ping_and_upsert(cfg) assert result.status == "ok" assert "2 models" in result.detail @pytest.mark.asyncio async def test_check_openrouter_ping_fail_401(monkeypatch: pytest.MonkeyPatch) -> None: import my_deepagent.cli.doctor as doctor_module monkeypatch.setattr(doctor_module, "resolve_openrouter_api_key", lambda cfg: "sk-bad") mock_response = MagicMock() mock_response.status_code = 401 http_err = httpx.HTTPStatusError("401", request=MagicMock(), response=mock_response) monkeypatch.setattr( doctor_module, "fetch_openrouter_pricing", AsyncMock(side_effect=http_err), ) cfg = MagicMock() result = await _check_openrouter_ping_and_upsert(cfg) assert result.status == "fail" assert "401" in result.detail @pytest.mark.asyncio async def test_check_openrouter_ping_warn_5xx(monkeypatch: pytest.MonkeyPatch) -> None: import my_deepagent.cli.doctor as doctor_module monkeypatch.setattr(doctor_module, "resolve_openrouter_api_key", lambda cfg: "sk-ok") mock_response = MagicMock() mock_response.status_code = 503 http_err = httpx.HTTPStatusError("503", request=MagicMock(), response=mock_response) monkeypatch.setattr( doctor_module, "fetch_openrouter_pricing", AsyncMock(side_effect=http_err), ) cfg = MagicMock() result = await _check_openrouter_ping_and_upsert(cfg) assert result.status == "warn" assert "503" in result.detail @pytest.mark.asyncio async def test_check_openrouter_ping_warn_empty_response( monkeypatch: pytest.MonkeyPatch, ) -> None: import my_deepagent.cli.doctor as doctor_module monkeypatch.setattr(doctor_module, "resolve_openrouter_api_key", lambda cfg: "sk-ok") monkeypatch.setattr( doctor_module, "fetch_openrouter_pricing", AsyncMock(return_value=[]), ) cfg = MagicMock() result = await _check_openrouter_ping_and_upsert(cfg) assert result.status == "warn" assert "no models" in result.detail # --------------------------------------------------------------------------- # 8. _check_disk_and_db (async) # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_check_disk_and_db_ok(tmp_path: Path) -> None: cfg = MagicMock() cfg.workspace_root = tmp_path cfg.database_url = f"sqlite+aiosqlite:///{tmp_path}/test.sqlite3" result = await _check_disk_and_db(cfg) # Should be ok or warn depending on actual free space — never fail in tmp assert result.status in ("ok", "warn") assert "sqlite_integrity=ok" in result.detail @pytest.mark.asyncio async def test_check_disk_and_db_warn_low_disk( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: # Simulate 5 GB free (warn zone: 2GB <= free < 10GB) class _FakeUsage: free: int = 5 * 1024**3 total: int = 100 * 1024**3 used: int = 95 * 1024**3 monkeypatch.setattr(shutil, "disk_usage", lambda _: _FakeUsage()) cfg = MagicMock() cfg.workspace_root = tmp_path cfg.database_url = f"sqlite+aiosqlite:///{tmp_path}/test.sqlite3" result = await _check_disk_and_db(cfg) assert result.status == "warn" assert "5.0GB" in result.detail