import asyncio from unittest.mock import AsyncMock, patch, MagicMock from db.models import AIProviderProfile from core.llm import LLMClient from services.admin_bot import get_persian_main_menu, AdminBotService async def test_system_hub_pause_toggle(): repo = AsyncMock() repo.is_system_paused.return_value = False admin_bot = AdminBotService(repo=repo, ai_processor=None, queue=None, review_channel_id=-100) text, buttons = await admin_bot._render_system_hub() btn_texts = [getattr(b, 'text', '') for row in buttons for b in row] assert any("🛑 توقف" in t for t in btn_texts) repo.is_system_paused.return_value = True text, buttons = await admin_bot._render_system_hub() btn_texts = [getattr(b, 'text', '') for row in buttons for b in row] assert any("▶️ راه‌اندازی" in t for t in btn_texts) async def test_llm_fallback_chain_success_on_fallback(): # Setup 2 providers: Primary (fails) -> Fallback (succeeds) profile_1 = AIProviderProfile( id=1, name="Primary AI", provider_type="openai", model="gpt-4o", base_url="https://api.openai.com/v1", api_key="key1", is_active=True, fallback_provider_id=2 ) profile_2 = AIProviderProfile( id=2, name="Backup AI", provider_type="gemini", model="gemini-1.5-flash", base_url="", api_key="key2", is_active=False, fallback_provider_id=None ) repo_mock = AsyncMock() repo_mock.get_active_provider_profile.return_value = profile_1 repo_mock.get_provider_profiles.return_value = [profile_1, profile_2] repo_mock.record_ai_log = AsyncMock() fallback_alerts = [] async def on_fallback(from_p, to_p, err, step): fallback_alerts.append((from_p.name, to_p.name, err, step)) client = LLMClient( repo=repo_mock, on_fallback_alert=on_fallback, ) client.max_retries_per_model = 0 # fail fast for test # Mock _call_openai to fail and _call_gemini to succeed with patch.object(client, "_call_openai", side_effect=RuntimeError("Primary Provider Timeout")): with patch.object(client, "_call_gemini", return_value={"decision": "accept", "rewritten_text": "Success from backup"}): res = await client.generate_json("Test prompt", system_prompt="Test sys") assert res["decision"] == "accept" assert res["rewritten_text"] == "Success from backup" assert len(fallback_alerts) == 1 assert fallback_alerts[0][0] == "Primary AI" assert fallback_alerts[0][1] == "Backup AI" assert "Primary Provider Timeout" in fallback_alerts[0][2] assert fallback_alerts[0][3] == 1 async def test_llm_fallback_chain_all_fail_alert(): profile_1 = AIProviderProfile( id=1, name="Primary AI", provider_type="openai", model="gpt-4o", is_active=True, fallback_provider_id=2 ) profile_2 = AIProviderProfile( id=2, name="Backup AI 1", provider_type="openai", model="gpt-4o-mini", is_active=False, fallback_provider_id=3 ) profile_3 = AIProviderProfile( id=3, name="Backup AI 2", provider_type="gemini", model="gemini-1.5-flash", is_active=False, fallback_provider_id=None ) repo_mock = AsyncMock() repo_mock.get_active_provider_profile.return_value = profile_1 repo_mock.get_provider_profiles.return_value = [profile_1, profile_2, profile_3] repo_mock.record_ai_log = AsyncMock() fallback_alerts = [] chain_failure_alerts = [] async def on_fallback(from_p, to_p, err, step): fallback_alerts.append((from_p.name, to_p.name, err, step)) async def on_chain_failure(chain, err): chain_failure_alerts.append((len(chain), err)) client = LLMClient( repo=repo_mock, on_fallback_alert=on_fallback, on_chain_failure_alert=on_chain_failure, ) client.max_retries_per_model = 0 with patch.object(client, "_call_openai", side_effect=RuntimeError("OpenAI Error")): with patch.object(client, "_call_gemini", side_effect=RuntimeError("Gemini Quota Exceeded")): try: await client.generate_json("Test prompt") assert False, "Should have raised exception" except Exception as e: assert "Gemini Quota Exceeded" in str(e) or "OpenAI Error" in str(e) # Check fallbacks: 1 -> 2, 2 -> 3 assert len(fallback_alerts) == 2 assert fallback_alerts[0][0] == "Primary AI" assert fallback_alerts[0][1] == "Backup AI 1" assert fallback_alerts[1][0] == "Backup AI 1" assert fallback_alerts[1][1] == "Backup AI 2" # Check chain failure alert triggered assert len(chain_failure_alerts) == 1 assert chain_failure_alerts[0][0] == 3 async def main(): await test_system_hub_pause_toggle() await test_llm_fallback_chain_success_on_fallback() await test_llm_fallback_chain_all_fail_alert() print("All system stop and multi-provider AI fallback chain tests passed successfully!") if __name__ == "__main__": asyncio.run(main())