|
|
|
import pytest |
|
import pandas as pd |
|
from unittest.mock import patch, MagicMock, ANY |
|
import gradio as gr |
|
from openai import OpenAIError |
|
|
|
|
|
from ankigen_core.learning_path import analyze_learning_path |
|
from ankigen_core.llm_interface import OpenAIClientManager |
|
from ankigen_core.utils import ResponseCache |
|
|
|
|
|
@pytest.fixture |
|
def mock_client_manager_learning_path(): |
|
"""Provides a mock OpenAIClientManager for learning path tests.""" |
|
manager = MagicMock(spec=OpenAIClientManager) |
|
mock_client = MagicMock() |
|
manager.get_client.return_value = mock_client |
|
manager.initialize_client.return_value = None |
|
return manager, mock_client |
|
|
|
|
|
@pytest.fixture |
|
def mock_response_cache_learning_path(): |
|
"""Provides a mock ResponseCache for learning path tests.""" |
|
cache = MagicMock(spec=ResponseCache) |
|
cache.get.return_value = None |
|
return cache |
|
|
|
|
|
@patch("ankigen_core.learning_path.structured_output_completion") |
|
def test_analyze_learning_path_success( |
|
mock_soc, mock_client_manager_learning_path, mock_response_cache_learning_path |
|
): |
|
"""Test successful learning path analysis.""" |
|
manager, client = mock_client_manager_learning_path |
|
cache = mock_response_cache_learning_path |
|
api_key = "valid_key" |
|
description = "Learn Python for data science" |
|
model = "gpt-test" |
|
|
|
|
|
mock_response = { |
|
"subjects": [ |
|
{ |
|
"Subject": "Python Basics", |
|
"Prerequisites": "None", |
|
"Time Estimate": "2 weeks", |
|
}, |
|
{ |
|
"Subject": "Pandas", |
|
"Prerequisites": "Python Basics", |
|
"Time Estimate": "1 week", |
|
}, |
|
], |
|
"learning_order": "Start with Basics, then move to Pandas.", |
|
"projects": "Analyze a sample dataset.", |
|
} |
|
mock_soc.return_value = mock_response |
|
|
|
df_result, order_text, projects_text = analyze_learning_path( |
|
client_manager=manager, |
|
cache=cache, |
|
api_key=api_key, |
|
description=description, |
|
model=model, |
|
) |
|
|
|
|
|
manager.initialize_client.assert_called_once_with(api_key) |
|
manager.get_client.assert_called_once() |
|
mock_soc.assert_called_once_with( |
|
openai_client=client, |
|
model=model, |
|
response_format={"type": "json_object"}, |
|
system_prompt=ANY, |
|
user_prompt=ANY, |
|
cache=cache, |
|
) |
|
|
|
assert isinstance(df_result, pd.DataFrame) |
|
assert len(df_result) == 2 |
|
assert list(df_result.columns) == ["Subject", "Prerequisites", "Time Estimate"] |
|
assert df_result.iloc[0]["Subject"] == "Python Basics" |
|
assert df_result.iloc[1]["Subject"] == "Pandas" |
|
|
|
assert "Recommended Learning Order" in order_text |
|
assert "Start with Basics, then move to Pandas." in order_text |
|
|
|
assert "Suggested Projects" in projects_text |
|
assert "Analyze a sample dataset." in projects_text |
|
|
|
|
|
def test_analyze_learning_path_no_api_key( |
|
mock_client_manager_learning_path, mock_response_cache_learning_path |
|
): |
|
"""Test that gr.Error is raised if API key is missing.""" |
|
manager, _ = mock_client_manager_learning_path |
|
cache = mock_response_cache_learning_path |
|
|
|
with pytest.raises(gr.Error, match="API key is required"): |
|
analyze_learning_path( |
|
client_manager=manager, |
|
cache=cache, |
|
api_key="", |
|
description="Test", |
|
model="gpt-test", |
|
) |
|
|
|
|
|
def test_analyze_learning_path_client_init_error( |
|
mock_client_manager_learning_path, mock_response_cache_learning_path |
|
): |
|
"""Test that gr.Error is raised if client initialization fails.""" |
|
manager, _ = mock_client_manager_learning_path |
|
cache = mock_response_cache_learning_path |
|
error_msg = "Invalid Key" |
|
manager.initialize_client.side_effect = ValueError(error_msg) |
|
|
|
with pytest.raises(gr.Error, match=f"OpenAI Client Error: {error_msg}"): |
|
analyze_learning_path( |
|
client_manager=manager, |
|
cache=cache, |
|
api_key="invalid_key", |
|
description="Test", |
|
model="gpt-test", |
|
) |
|
|
|
|
|
@patch("ankigen_core.learning_path.structured_output_completion") |
|
def test_analyze_learning_path_api_error( |
|
mock_soc, mock_client_manager_learning_path, mock_response_cache_learning_path |
|
): |
|
"""Test that errors from structured_output_completion are handled.""" |
|
manager, _ = mock_client_manager_learning_path |
|
cache = mock_response_cache_learning_path |
|
error_msg = "API connection failed" |
|
mock_soc.side_effect = OpenAIError(error_msg) |
|
|
|
with pytest.raises(gr.Error, match=f"Failed to analyze learning path: {error_msg}"): |
|
analyze_learning_path( |
|
client_manager=manager, |
|
cache=cache, |
|
api_key="valid_key", |
|
description="Test", |
|
model="gpt-test", |
|
) |
|
|
|
|
|
@patch("ankigen_core.learning_path.structured_output_completion") |
|
def test_analyze_learning_path_invalid_response_format( |
|
mock_soc, mock_client_manager_learning_path, mock_response_cache_learning_path |
|
): |
|
"""Test handling of invalid response format from API.""" |
|
manager, _ = mock_client_manager_learning_path |
|
cache = mock_response_cache_learning_path |
|
|
|
|
|
invalid_responses = [ |
|
None, |
|
"just a string", |
|
{}, |
|
{"subjects": "not a list"}, |
|
{"subjects": [], "learning_order": "Order"}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
] |
|
|
|
for mock_response in invalid_responses: |
|
mock_soc.reset_mock() |
|
mock_soc.return_value = mock_response |
|
with pytest.raises(gr.Error, match="invalid API response format"): |
|
analyze_learning_path( |
|
client_manager=manager, |
|
cache=cache, |
|
api_key="valid_key", |
|
description="Test Invalid", |
|
model="gpt-test", |
|
) |
|
|
|
|
|
@patch("ankigen_core.learning_path.structured_output_completion") |
|
def test_analyze_learning_path_no_valid_subjects( |
|
mock_soc, mock_client_manager_learning_path, mock_response_cache_learning_path |
|
): |
|
"""Test handling when API returns subjects but none are valid.""" |
|
manager, _ = mock_client_manager_learning_path |
|
cache = mock_response_cache_learning_path |
|
|
|
mock_response = { |
|
"subjects": [{"wrong_key": "value"}, {}], |
|
"learning_order": "Order", |
|
"projects": "Projects", |
|
} |
|
mock_soc.return_value = mock_response |
|
|
|
with pytest.raises(gr.Error, match="API returned no valid subjects"): |
|
analyze_learning_path( |
|
client_manager=manager, |
|
cache=cache, |
|
api_key="valid_key", |
|
description="Test No Valid Subjects", |
|
model="gpt-test", |
|
) |
|
|
|
|
|
@patch("ankigen_core.learning_path.structured_output_completion") |
|
def test_analyze_learning_path_invalid_subject_structure( |
|
mock_soc, mock_client_manager_learning_path, mock_response_cache_learning_path |
|
): |
|
"""Test handling when subjects list contains ONLY invalid/incomplete dicts.""" |
|
manager, _ = mock_client_manager_learning_path |
|
cache = mock_response_cache_learning_path |
|
|
|
|
|
invalid_subject_responses = [ |
|
{ |
|
"subjects": [{"Subject": "S1"}], |
|
"learning_order": "O", |
|
"projects": "P", |
|
}, |
|
{ |
|
"subjects": ["invalid_string"], |
|
"learning_order": "O", |
|
"projects": "P", |
|
}, |
|
{ |
|
"subjects": [{"wrong_key": "value"}], |
|
"learning_order": "O", |
|
"projects": "P", |
|
}, |
|
] |
|
|
|
for mock_response in invalid_subject_responses: |
|
mock_soc.reset_mock() |
|
mock_soc.return_value = mock_response |
|
with pytest.raises(gr.Error, match="API returned no valid subjects"): |
|
analyze_learning_path( |
|
client_manager=manager, |
|
cache=cache, |
|
api_key="valid_key", |
|
description="Test Invalid Subject Structure", |
|
model="gpt-test", |
|
) |
|
|