Files
resolutionflow/backend/tests/test_ai_chat.py
chihlasm f31058fc3f fix: resolve CI failures — ESLint unused vars and AI chat tests in CI
Frontend: suppress unused-vars ESLint errors for callback params in
MaintenanceFlowDetailPage and StepLibraryPage.

Backend: add autouse fixture to mock settings.ai_enabled=True in
test_ai_chat.py so tests pass in CI where no ANTHROPIC_API_KEY is set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 14:21:48 -05:00

199 lines
7.3 KiB
Python

"""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