"""Tests for sub_query module (RLM-style recursive reasoning)."""
from __future__ import annotations
import asyncio
import os
from unittest.mock import AsyncMock, patch, MagicMock
import pytest
from aleph.sub_query import SubQueryConfig, detect_backend, has_api_credentials
from aleph.sub_query.cli_backend import run_cli_sub_query, CLI_BACKENDS
from aleph.sub_query.api_backend import run_api_sub_query
class TestSubQueryConfig:
"""Tests for SubQueryConfig."""
def test_default_config(self):
config = SubQueryConfig()
assert config.backend == "auto"
assert config.api_model == "mimo-v2-flash"
assert config.api_key_env == "MIMO_API_KEY"
assert config.api_base_url_env == "OPENAI_BASE_URL"
assert config.max_context_chars == 100_000
def test_custom_config(self):
config = SubQueryConfig(
backend="api",
api_model="gpt-4o-mini",
max_context_chars=50_000,
)
assert config.backend == "api"
assert config.api_model == "gpt-4o-mini"
assert config.max_context_chars == 50_000
class TestDetectBackend:
"""Tests for backend detection.
Priority order (API-first):
1. ALEPH_SUB_QUERY_BACKEND env var (explicit override)
2. API (if credentials available)
3. claude CLI (if installed)
4. codex CLI (if installed)
5. aider CLI (if installed)
6. API fallback (will error with helpful message)
"""
def test_detect_backend_api_preferred_when_credentials_available(self):
"""API should be preferred when credentials are set, even if CLI is available."""
with patch.dict(os.environ, {"MIMO_API_KEY": "test-key"}):
with patch("aleph.sub_query.shutil.which") as mock_which:
mock_which.side_effect = lambda x: "/usr/bin/claude" if x == "claude" else None
assert detect_backend() == "api"
def test_detect_backend_openai_key_also_works(self):
"""OPENAI_API_KEY should also trigger API preference."""
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
# Clear MIMO_API_KEY if set
env = dict(os.environ)
env.pop("MIMO_API_KEY", None)
env["OPENAI_API_KEY"] = "test-key"
with patch.dict(os.environ, env, clear=True):
with patch("aleph.sub_query.shutil.which") as mock_which:
mock_which.side_effect = lambda x: "/usr/bin/claude" if x == "claude" else None
assert detect_backend() == "api"
def test_detect_backend_explicit_override(self):
"""ALEPH_SUB_QUERY_BACKEND should override all other detection."""
with patch.dict(os.environ, {"ALEPH_SUB_QUERY_BACKEND": "codex", "MIMO_API_KEY": "key"}):
with patch("aleph.sub_query.shutil.which") as mock_which:
mock_which.return_value = "/usr/bin/something"
assert detect_backend() == "codex"
def test_detect_backend_explicit_override_api(self):
"""ALEPH_SUB_QUERY_BACKEND=api should force API even without credentials."""
with patch.dict(os.environ, {"ALEPH_SUB_QUERY_BACKEND": "api"}, clear=False):
env = dict(os.environ)
env.pop("MIMO_API_KEY", None)
env.pop("OPENAI_API_KEY", None)
env["ALEPH_SUB_QUERY_BACKEND"] = "api"
with patch.dict(os.environ, env, clear=True):
assert detect_backend() == "api"
def test_detect_backend_claude_when_no_api_credentials(self):
"""Claude CLI should be used when no API credentials are available."""
with patch.dict(os.environ, {}, clear=True):
with patch("aleph.sub_query.shutil.which") as mock_which:
mock_which.side_effect = lambda x: "/usr/bin/claude" if x == "claude" else None
assert detect_backend() == "claude"
def test_detect_backend_codex_when_no_api_credentials(self):
"""Codex CLI should be used when no API credentials and no Claude."""
with patch.dict(os.environ, {}, clear=True):
with patch("aleph.sub_query.shutil.which") as mock_which:
mock_which.side_effect = lambda x: "/usr/bin/codex" if x == "codex" else None
assert detect_backend() == "codex"
def test_detect_backend_aider_when_no_api_credentials(self):
"""Aider CLI should be used when no API credentials and no Claude/Codex."""
with patch.dict(os.environ, {}, clear=True):
with patch("aleph.sub_query.shutil.which") as mock_which:
mock_which.side_effect = lambda x: "/usr/bin/aider" if x == "aider" else None
assert detect_backend() == "aider"
def test_detect_backend_api_fallback(self):
"""API fallback when nothing else available (will error with helpful message)."""
with patch.dict(os.environ, {}, clear=True):
with patch("aleph.sub_query.shutil.which") as mock_which:
mock_which.return_value = None
assert detect_backend() == "api"
def test_detect_backend_model_override_implies_api(self):
"""ALEPH_SUB_QUERY_MODEL should prefer API when credentials available."""
with patch.dict(os.environ, {"ALEPH_SUB_QUERY_MODEL": "gpt-4o", "OPENAI_API_KEY": "key"}):
with patch("aleph.sub_query.shutil.which") as mock_which:
mock_which.side_effect = lambda x: "/usr/bin/claude" if x == "claude" else None
assert detect_backend() == "api"
class TestHasApiCredentials:
"""Tests for API credential detection."""
def test_has_credentials(self):
with patch.dict(os.environ, {"MIMO_API_KEY": "test-key"}):
assert has_api_credentials() is True
def test_no_credentials(self):
with patch.dict(os.environ, {}, clear=True):
# Remove both API keys if they exist
os.environ.pop("MIMO_API_KEY", None)
os.environ.pop("OPENAI_API_KEY", None)
assert has_api_credentials() is False
class TestCliBackend:
"""Tests for CLI backend."""
@pytest.mark.asyncio
async def test_cli_not_found(self):
with patch("asyncio.create_subprocess_exec") as mock_exec:
mock_exec.side_effect = FileNotFoundError("claude not found")
success, output = await run_cli_sub_query(
prompt="test",
backend="claude",
)
assert success is False
assert "not found" in output.lower()
@pytest.mark.asyncio
async def test_cli_success(self):
mock_proc = AsyncMock()
mock_proc.returncode = 0
mock_proc.communicate = AsyncMock(return_value=(b"Test response", b""))
with patch("asyncio.create_subprocess_exec", return_value=mock_proc):
success, output = await run_cli_sub_query(
prompt="test prompt",
backend="claude",
timeout=10.0,
)
assert success is True
assert output == "Test response"
@pytest.mark.asyncio
async def test_cli_timeout(self):
mock_proc = AsyncMock()
mock_proc.communicate = AsyncMock(side_effect=asyncio.TimeoutError())
mock_proc.kill = MagicMock()
mock_proc.wait = AsyncMock()
with patch("asyncio.create_subprocess_exec", return_value=mock_proc):
success, output = await run_cli_sub_query(
prompt="test",
backend="claude",
timeout=0.1,
)
assert success is False
assert "timeout" in output.lower()
@pytest.mark.asyncio
async def test_cli_with_context(self):
mock_proc = AsyncMock()
mock_proc.returncode = 0
mock_proc.communicate = AsyncMock(return_value=(b"Result with context", b""))
with patch("asyncio.create_subprocess_exec", return_value=mock_proc) as mock_exec:
success, output = await run_cli_sub_query(
prompt="Summarize this:",
context_slice="Some important text here.",
backend="claude",
)
assert success is True
# Verify the command was called (exact args depend on backend)
mock_exec.assert_called_once()
class TestApiBackend:
"""Tests for API backend."""
@pytest.mark.asyncio
async def test_api_no_key(self):
with patch.dict(os.environ, {}, clear=True):
os.environ.pop("OPENAI_API_KEY", None)
success, output = await run_api_sub_query(
prompt="test",
)
assert success is False
assert "API key not found" in output
@pytest.mark.asyncio
async def test_api_success(self):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"choices": [{"message": {"content": "API response"}}]
}
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key", "OPENAI_BASE_URL": "https://api.test.com/v1"}):
with patch("httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_instance.post = AsyncMock(return_value=mock_response)
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
mock_instance.__aexit__ = AsyncMock(return_value=None)
mock_client.return_value = mock_instance
success, output = await run_api_sub_query(
prompt="test prompt",
)
assert success is True
assert output == "API response"
@pytest.mark.asyncio
async def test_api_error_response(self):
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.text = "Internal Server Error"
mock_response.json.return_value = {"error": {"message": "Server error"}}
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}):
with patch("httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_instance.post = AsyncMock(return_value=mock_response)
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
mock_instance.__aexit__ = AsyncMock(return_value=None)
mock_client.return_value = mock_instance
success, output = await run_api_sub_query(prompt="test")
assert success is False
assert "500" in output
@pytest.mark.asyncio
async def test_api_with_system_prompt(self):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"choices": [{"message": {"content": "Response"}}]
}
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}):
with patch("httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_instance.post = AsyncMock(return_value=mock_response)
mock_instance.__aenter__ = AsyncMock(return_value=mock_instance)
mock_instance.__aexit__ = AsyncMock(return_value=None)
mock_client.return_value = mock_instance
success, output = await run_api_sub_query(
prompt="test",
system_prompt="You are a helpful assistant.",
)
assert success is True
# Verify system prompt was included
call_args = mock_instance.post.call_args
payload = call_args.kwargs.get("json", call_args.args[1] if len(call_args.args) > 1 else {})
messages = payload.get("messages", [])
assert any(m.get("role") == "system" for m in messages)
class TestCliBackends:
"""Tests for CLI_BACKENDS constant."""
def test_cli_backends_tuple(self):
assert isinstance(CLI_BACKENDS, tuple)
assert "claude" in CLI_BACKENDS
assert "codex" in CLI_BACKENDS
assert "aider" in CLI_BACKENDS