"""Integration tests for AI Chat Builder endpoints. These tests mock the AI provider to avoid real API calls. """ import pytest from unittest.mock import AsyncMock, patch, PropertyMock pytestmark = pytest.mark.asyncio @pytest.fixture(autouse=True) def _enable_ai(): """Ensure ai_enabled returns True even without API keys in CI.""" with patch( "app.core.config.Settings.ai_enabled", new_callable=PropertyMock, return_value=True, ): yield @pytest.fixture def mock_ai_provider(): """Mock AI provider that returns realistic responses.""" provider = AsyncMock() provider.generate_text = AsyncMock(return_value=( "Great question! Let's build a troubleshooting flow for DNS resolution issues. " "To start, I need to understand the scope.\n\n" "Who is the target audience for this flow? Are we targeting:\n" "- Tier 1 help desk (basic checks only)\n" "- Tier 2 desktop support (intermediate diagnostics)\n" "- Tier 3 systems engineers (deep DNS troubleshooting)\n\n" "[PHASE:scoping]", 500, # input tokens 200, # output tokens )) return provider async def test_create_chat_session(client, auth_headers, mock_ai_provider): """POST /ai/chat/sessions creates a session and returns AI greeting.""" with patch("app.core.ai_chat_service.get_ai_provider", return_value=mock_ai_provider): resp = await client.post( "/api/v1/ai/chat/sessions", json={"flow_type": "troubleshooting"}, headers=auth_headers, ) assert resp.status_code == 201 data = resp.json() assert "session_id" in data assert "greeting" in data assert data["current_phase"] == "scoping" assert len(data["greeting"]) > 0 async def test_send_message(client, auth_headers, mock_ai_provider): """POST /ai/chat/sessions/{id}/messages returns AI response.""" # Create session first with patch("app.core.ai_chat_service.get_ai_provider", return_value=mock_ai_provider): create_resp = await client.post( "/api/v1/ai/chat/sessions", json={"flow_type": "troubleshooting"}, headers=auth_headers, ) session_id = create_resp.json()["session_id"] # Mock response with tree update — must pass validate_generated_tree (min 5 nodes) import json tree_obj = { "id": "root", "type": "decision", "question": "What DNS symptom is the user experiencing?", "options": [ {"id": "opt-1", "label": "Cannot resolve any domains", "next_node_id": "dns-check"}, {"id": "opt-2", "label": "Intermittent failures", "next_node_id": "dns-cache-fix"}, ], "children": [ { "id": "dns-check", "type": "decision", "question": "Is the DNS Client service running?", "options": [ {"id": "dc-1", "label": "Yes", "next_node_id": "dns-fwd-fix"}, {"id": "dc-2", "label": "No", "next_node_id": "dns-svc-fix"}, ], "children": [ {"id": "dns-fwd-fix", "type": "solution", "title": "Check DNS Forwarders", "description": "DNS forwarders may be misconfigured", "resolution_steps": ["Check forwarder config"]}, {"id": "dns-svc-fix", "type": "solution", "title": "Restart DNS Service", "description": "DNS Client service is stopped", "resolution_steps": ["Start-Service Dnscache"]}, ], }, {"id": "dns-cache-fix", "type": "solution", "title": "Stale DNS Cache", "description": "DNS cache has stale entries", "resolution_steps": ["ipconfig /flushdns"]}, ], } tree_json = json.dumps(tree_obj) mock_ai_provider.generate_text = AsyncMock(return_value=( "Good, targeting Tier 2 support. Let's start with the first diagnostic question.\n\n" "The root question should be: 'What DNS symptom is the user experiencing?'\n\n" f"[TREE_UPDATE]\n{tree_json}\n[/TREE_UPDATE]\n\n" "[PHASE:discovery]", 800, 400, )) with patch("app.core.ai_chat_service.get_ai_provider", return_value=mock_ai_provider): resp = await client.post( f"/api/v1/ai/chat/sessions/{session_id}/messages", json={"content": "This is for Tier 2 support, hybrid environment with on-prem AD."}, headers=auth_headers, ) assert resp.status_code == 200 data = resp.json() assert "content" in data assert data["current_phase"] == "discovery" assert data["working_tree"] is not None assert data["working_tree"]["type"] == "decision" # Markers should be stripped from content assert "[TREE_UPDATE]" not in data["content"] assert "[PHASE:" not in data["content"] async def test_get_session(client, auth_headers, mock_ai_provider): """GET /ai/chat/sessions/{id} returns full session state.""" with patch("app.core.ai_chat_service.get_ai_provider", return_value=mock_ai_provider): create_resp = await client.post( "/api/v1/ai/chat/sessions", json={"flow_type": "troubleshooting"}, headers=auth_headers, ) session_id = create_resp.json()["session_id"] resp = await client.get( f"/api/v1/ai/chat/sessions/{session_id}", headers=auth_headers, ) assert resp.status_code == 200 data = resp.json() assert data["session_id"] == session_id assert data["status"] == "active" assert data["flow_type"] == "troubleshooting" # Hidden primer message should be filtered out assert all( msg.get("role") == "assistant" or not msg.get("hidden") for msg in data["conversation_history"] ) async def test_abandon_session(client, auth_headers, mock_ai_provider): """DELETE /ai/chat/sessions/{id} sets status to abandoned.""" with patch("app.core.ai_chat_service.get_ai_provider", return_value=mock_ai_provider): create_resp = await client.post( "/api/v1/ai/chat/sessions", json={"flow_type": "troubleshooting"}, headers=auth_headers, ) session_id = create_resp.json()["session_id"] resp = await client.delete( f"/api/v1/ai/chat/sessions/{session_id}", headers=auth_headers, ) assert resp.status_code == 204 # Verify session is abandoned get_resp = await client.get( f"/api/v1/ai/chat/sessions/{session_id}", headers=auth_headers, ) assert get_resp.json()["status"] == "abandoned" async def test_session_not_found(client, auth_headers): """Accessing nonexistent session returns 404.""" import uuid fake_id = str(uuid.uuid4()) resp = await client.get( f"/api/v1/ai/chat/sessions/{fake_id}", headers=auth_headers, ) assert resp.status_code == 404 async def test_ai_disabled_returns_503(client, auth_headers): """When AI is not configured, endpoints return 503.""" with patch("app.api.endpoints.ai_chat.settings") as mock_settings: mock_settings.ai_enabled = False resp = await client.post( "/api/v1/ai/chat/sessions", json={"flow_type": "troubleshooting"}, headers=auth_headers, ) assert resp.status_code == 503