Files
resolutionflow/backend/tests/test_ai_chat.py
chihlasm 0da67586da feat: add backend tests for AI chat builder + fix conversation_id FK issue
Tests cover session create, send message with tree update, get session,
abandon, 404 on missing session, and 503 when AI disabled.

Fixed: ai_usage.conversation_id has FK to ai_conversations, not
ai_chat_sessions. Chat builder now passes conversation_id=None and
tracks session reference in extra_data.chat_session_id.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 04:06:47 -05:00

188 lines
7.0 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
pytestmark = pytest.mark.asyncio
@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