mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-22 03:31:02 +02:00
Test updates, still not working
This commit is contained in:
parent
e434e03cfa
commit
0d8b1aff57
3 changed files with 957 additions and 0 deletions
202
tests/integration/test_template_service_integration.py
Normal file
202
tests/integration/test_template_service_integration.py
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
"""
|
||||
Simplified integration tests for Template Service
|
||||
|
||||
These tests verify the basic functionality of the template service
|
||||
without the full message queue infrastructure.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from trustgraph.schema import PromptRequest, PromptResponse
|
||||
from trustgraph.template.prompt_manager import PromptManager
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestTemplateServiceSimple:
|
||||
"""Simplified integration tests for Template Service components"""
|
||||
|
||||
@pytest.fixture
|
||||
def sample_config(self):
|
||||
"""Sample configuration for testing"""
|
||||
return {
|
||||
"system": json.dumps("You are a helpful assistant."),
|
||||
"template-index": json.dumps(["greeting", "json_test"]),
|
||||
"template.greeting": json.dumps({
|
||||
"prompt": "Hello {{ name }}, welcome to {{ system_name }}!",
|
||||
"response-type": "text"
|
||||
}),
|
||||
"template.json_test": json.dumps({
|
||||
"prompt": "Generate profile for {{ username }}",
|
||||
"response-type": "json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"role": {"type": "string"}
|
||||
},
|
||||
"required": ["name", "role"]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def prompt_manager(self, sample_config):
|
||||
"""Create a configured PromptManager"""
|
||||
pm = PromptManager()
|
||||
pm.load_config(sample_config)
|
||||
pm.terms["system_name"] = "TrustGraph"
|
||||
return pm
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_manager_text_invocation(self, prompt_manager):
|
||||
"""Test PromptManager text response invocation"""
|
||||
# Mock LLM function
|
||||
async def mock_llm(system, prompt):
|
||||
assert system == "You are a helpful assistant."
|
||||
assert "Hello Alice, welcome to TrustGraph!" in prompt
|
||||
return "Welcome message processed!"
|
||||
|
||||
result = await prompt_manager.invoke("greeting", {"name": "Alice"}, mock_llm)
|
||||
|
||||
assert result == "Welcome message processed!"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_manager_json_invocation(self, prompt_manager):
|
||||
"""Test PromptManager JSON response invocation"""
|
||||
# Mock LLM function
|
||||
async def mock_llm(system, prompt):
|
||||
assert "Generate profile for johndoe" in prompt
|
||||
return '{"name": "John Doe", "role": "user"}'
|
||||
|
||||
result = await prompt_manager.invoke("json_test", {"username": "johndoe"}, mock_llm)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["name"] == "John Doe"
|
||||
assert result["role"] == "user"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_manager_json_validation_error(self, prompt_manager):
|
||||
"""Test JSON schema validation failure"""
|
||||
# Mock LLM function that returns invalid JSON
|
||||
async def mock_llm(system, prompt):
|
||||
return '{"name": "John Doe"}' # Missing required "role"
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await prompt_manager.invoke("json_test", {"username": "johndoe"}, mock_llm)
|
||||
|
||||
assert "Schema validation fail" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_manager_json_parse_error(self, prompt_manager):
|
||||
"""Test JSON parsing failure"""
|
||||
# Mock LLM function that returns non-JSON
|
||||
async def mock_llm(system, prompt):
|
||||
return "This is not JSON at all"
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await prompt_manager.invoke("json_test", {"username": "johndoe"}, mock_llm)
|
||||
|
||||
assert "JSON parse fail" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_manager_unknown_prompt(self, prompt_manager):
|
||||
"""Test unknown prompt ID handling"""
|
||||
async def mock_llm(system, prompt):
|
||||
return "Response"
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await prompt_manager.invoke("unknown_prompt", {}, mock_llm)
|
||||
|
||||
assert "ID invalid" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_manager_term_merging(self, prompt_manager):
|
||||
"""Test proper term merging (global + prompt + input)"""
|
||||
# Add prompt-specific terms
|
||||
prompt_manager.prompts["greeting"].terms = {"greeting_prefix": "Hi"}
|
||||
|
||||
async def mock_llm(system, prompt):
|
||||
# Should have global term (system_name), input term (name), and any prompt terms
|
||||
assert "TrustGraph" in prompt # Global term
|
||||
assert "Bob" in prompt # Input term
|
||||
return "Merged correctly"
|
||||
|
||||
result = await prompt_manager.invoke("greeting", {"name": "Bob"}, mock_llm)
|
||||
assert result == "Merged correctly"
|
||||
|
||||
def test_prompt_manager_template_rendering(self, prompt_manager):
|
||||
"""Test direct template rendering"""
|
||||
result = prompt_manager.render("greeting", {"name": "Charlie"})
|
||||
|
||||
assert "Hello Charlie, welcome to TrustGraph!" == result.strip()
|
||||
|
||||
def test_prompt_manager_configuration_loading(self):
|
||||
"""Test configuration loading with various formats"""
|
||||
pm = PromptManager()
|
||||
|
||||
# Test empty configuration
|
||||
pm.load_config({})
|
||||
assert pm.config.system_template == "Be helpful."
|
||||
assert len(pm.prompts) == 0
|
||||
|
||||
# Test configuration with single prompt
|
||||
config = {
|
||||
"system": json.dumps("Test system"),
|
||||
"template-index": json.dumps(["test"]),
|
||||
"template.test": json.dumps({
|
||||
"prompt": "Test {{ value }}",
|
||||
"response-type": "text"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
assert pm.config.system_template == "Test system"
|
||||
assert "test" in pm.prompts
|
||||
assert pm.prompts["test"].response_type == "text"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_manager_json_with_markdown(self, prompt_manager):
|
||||
"""Test JSON extraction from markdown code blocks"""
|
||||
async def mock_llm(system, prompt):
|
||||
return '''
|
||||
Here's the profile:
|
||||
```json
|
||||
{"name": "Jane Smith", "role": "admin"}
|
||||
```
|
||||
'''
|
||||
|
||||
result = await prompt_manager.invoke("json_test", {"username": "jane"}, mock_llm)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["name"] == "Jane Smith"
|
||||
assert result["role"] == "admin"
|
||||
|
||||
def test_prompt_manager_error_handling_in_templates(self, prompt_manager):
|
||||
"""Test error handling in template rendering"""
|
||||
# Test with missing variable
|
||||
with pytest.raises(Exception): # ibis should raise on undefined variable
|
||||
prompt_manager.render("greeting", {}) # Missing 'name'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_prompt_invocations(self, prompt_manager):
|
||||
"""Test concurrent invocations"""
|
||||
async def mock_llm(system, prompt):
|
||||
# Extract name from prompt for response
|
||||
if "Alice" in prompt:
|
||||
return "Alice response"
|
||||
elif "Bob" in prompt:
|
||||
return "Bob response"
|
||||
else:
|
||||
return "Default response"
|
||||
|
||||
# Run concurrent invocations
|
||||
import asyncio
|
||||
results = await asyncio.gather(
|
||||
prompt_manager.invoke("greeting", {"name": "Alice"}, mock_llm),
|
||||
prompt_manager.invoke("greeting", {"name": "Bob"}, mock_llm),
|
||||
)
|
||||
|
||||
assert "Alice response" in results
|
||||
assert "Bob response" in results
|
||||
327
tests/unit/test_prompt_manager.py
Normal file
327
tests/unit/test_prompt_manager.py
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
"""
|
||||
Unit tests for PromptManager
|
||||
|
||||
These tests verify the functionality of the PromptManager class,
|
||||
including template rendering, term merging, JSON validation, and error handling.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from trustgraph.template.prompt_manager import PromptManager, PromptConfiguration, Prompt
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPromptManager:
|
||||
"""Unit tests for PromptManager template functionality"""
|
||||
|
||||
@pytest.fixture
|
||||
def sample_config(self):
|
||||
"""Sample configuration dict for PromptManager"""
|
||||
return {
|
||||
"system": json.dumps("You are a helpful assistant."),
|
||||
"template-index": json.dumps(["simple_text", "json_response", "complex_template"]),
|
||||
"template.simple_text": json.dumps({
|
||||
"prompt": "Hello {{ name }}, welcome to {{ system_name }}!",
|
||||
"response-type": "text"
|
||||
}),
|
||||
"template.json_response": json.dumps({
|
||||
"prompt": "Generate a user profile for {{ username }}",
|
||||
"response-type": "json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "number"}
|
||||
},
|
||||
"required": ["name", "age"]
|
||||
}
|
||||
}),
|
||||
"template.complex_template": json.dumps({
|
||||
"prompt": """
|
||||
{% for item in items %}
|
||||
- {{ item.name }}: {{ item.value }}
|
||||
{% endfor %}
|
||||
Total: {{ items|length }}
|
||||
""",
|
||||
"response-type": "text"
|
||||
})
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def prompt_manager(self, sample_config):
|
||||
"""Create a PromptManager with sample configuration"""
|
||||
pm = PromptManager()
|
||||
pm.load_config(sample_config)
|
||||
# Add global terms manually since load_config doesn't handle them
|
||||
pm.terms["system_name"] = "TrustGraph"
|
||||
pm.terms["version"] = "1.0"
|
||||
return pm
|
||||
|
||||
def test_prompt_manager_initialization(self, prompt_manager, sample_config):
|
||||
"""Test PromptManager initialization with configuration"""
|
||||
assert prompt_manager.config.system_template == "You are a helpful assistant."
|
||||
assert len(prompt_manager.prompts) == 3
|
||||
assert "simple_text" in prompt_manager.prompts
|
||||
|
||||
def test_simple_text_template_rendering(self, prompt_manager):
|
||||
"""Test basic template rendering with text response"""
|
||||
terms = {"name": "Alice"}
|
||||
|
||||
rendered = prompt_manager.render("simple_text", terms)
|
||||
|
||||
assert rendered == "Hello Alice, welcome to TrustGraph!"
|
||||
|
||||
def test_global_terms_merging(self, prompt_manager):
|
||||
"""Test that global terms are properly merged"""
|
||||
terms = {"name": "Bob"}
|
||||
|
||||
# Global terms should be available in template
|
||||
rendered = prompt_manager.render("simple_text", terms)
|
||||
|
||||
assert "TrustGraph" in rendered # From global terms
|
||||
assert "Bob" in rendered # From input terms
|
||||
|
||||
def test_term_override_priority(self, prompt_manager):
|
||||
"""Test term override priority: input > prompt > global"""
|
||||
# Add a test prompt with overlapping terms
|
||||
test_config = {
|
||||
"template.test": json.dumps({
|
||||
"prompt": "Value is: {{ value }}",
|
||||
"response-type": "text"
|
||||
})
|
||||
}
|
||||
prompt_manager.load_config({**prompt_manager.config.__dict__, **test_config})
|
||||
prompt_manager.terms["value"] = "global"
|
||||
prompt_manager.prompts["test"].terms["value"] = "prompt"
|
||||
|
||||
# Test with no input override
|
||||
rendered = prompt_manager.render("test", {})
|
||||
assert rendered == "Value is: prompt" # Prompt terms override global
|
||||
|
||||
# Test with input override
|
||||
rendered = prompt_manager.render("test", {"value": "input"})
|
||||
assert rendered == "Value is: input" # Input terms override all
|
||||
|
||||
def test_complex_template_rendering(self, prompt_manager):
|
||||
"""Test complex template with loops and filters"""
|
||||
terms = {
|
||||
"items": [
|
||||
{"name": "Item1", "value": 10},
|
||||
{"name": "Item2", "value": 20},
|
||||
{"name": "Item3", "value": 30}
|
||||
]
|
||||
}
|
||||
|
||||
rendered = prompt_manager.render("complex_template", terms)
|
||||
|
||||
assert "Item1: 10" in rendered
|
||||
assert "Item2: 20" in rendered
|
||||
assert "Item3: 30" in rendered
|
||||
assert "Total: 3" in rendered
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_text_response(self, prompt_manager):
|
||||
"""Test invoking a prompt with text response"""
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = "Welcome Alice to TrustGraph!"
|
||||
|
||||
result = await prompt_manager.invoke(
|
||||
"simple_text",
|
||||
{"name": "Alice"},
|
||||
mock_llm
|
||||
)
|
||||
|
||||
assert result == "Welcome Alice to TrustGraph!"
|
||||
|
||||
# Verify LLM was called with correct prompts
|
||||
mock_llm.assert_called_once()
|
||||
call_args = mock_llm.call_args[1]
|
||||
assert call_args["system"] == "You are a helpful assistant."
|
||||
assert "Hello Alice, welcome to TrustGraph!" in call_args["prompt"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_json_response_valid(self, prompt_manager):
|
||||
"""Test invoking a prompt with valid JSON response"""
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = '{"name": "John Doe", "age": 30}'
|
||||
|
||||
result = await prompt_manager.invoke(
|
||||
"json_response",
|
||||
{"username": "johndoe"},
|
||||
mock_llm
|
||||
)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["name"] == "John Doe"
|
||||
assert result["age"] == 30
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_json_response_with_markdown(self, prompt_manager):
|
||||
"""Test JSON extraction from markdown code blocks"""
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = """
|
||||
Here is the user profile:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Jane Smith",
|
||||
"age": 25
|
||||
}
|
||||
```
|
||||
|
||||
This is a valid profile.
|
||||
"""
|
||||
|
||||
result = await prompt_manager.invoke(
|
||||
"json_response",
|
||||
{"username": "janesmith"},
|
||||
mock_llm
|
||||
)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["name"] == "Jane Smith"
|
||||
assert result["age"] == 25
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_json_validation_failure(self, prompt_manager):
|
||||
"""Test JSON schema validation failure"""
|
||||
mock_llm = AsyncMock()
|
||||
# Missing required 'age' field
|
||||
mock_llm.return_value = '{"name": "Invalid User"}'
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await prompt_manager.invoke(
|
||||
"json_response",
|
||||
{"username": "invalid"},
|
||||
mock_llm
|
||||
)
|
||||
|
||||
assert "JSON schema validation failed" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_json_parse_failure(self, prompt_manager):
|
||||
"""Test invalid JSON parsing"""
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = "This is not JSON at all"
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await prompt_manager.invoke(
|
||||
"json_response",
|
||||
{"username": "test"},
|
||||
mock_llm
|
||||
)
|
||||
|
||||
assert "No JSON found in response" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_unknown_prompt(self, prompt_manager):
|
||||
"""Test invoking an unknown prompt ID"""
|
||||
mock_llm = AsyncMock()
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
await prompt_manager.invoke(
|
||||
"nonexistent_prompt",
|
||||
{},
|
||||
mock_llm
|
||||
)
|
||||
|
||||
def test_template_rendering_with_undefined_variable(self, prompt_manager):
|
||||
"""Test template rendering with undefined variables"""
|
||||
terms = {} # Missing 'name' variable
|
||||
|
||||
# This should raise an error or use empty string depending on config
|
||||
with pytest.raises(Exception): # ibis/Jinja2 will raise on undefined
|
||||
prompt_manager.render("simple_text", terms)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_json_response_without_schema(self):
|
||||
"""Test JSON response without schema validation"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["no_schema"]),
|
||||
"template.no_schema": json.dumps({
|
||||
"prompt": "Generate any JSON",
|
||||
"response-type": "json"
|
||||
# No schema defined
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = '{"any": "json", "is": "valid"}'
|
||||
|
||||
result = await pm.invoke("no_schema", {}, mock_llm)
|
||||
|
||||
assert result == {"any": "json", "is": "valid"}
|
||||
|
||||
def test_prompt_configuration_validation(self):
|
||||
"""Test PromptConfiguration validation"""
|
||||
# Valid configuration
|
||||
config = PromptConfiguration(
|
||||
system_template="Test system",
|
||||
prompts={
|
||||
"test": Prompt(
|
||||
template="Hello {{ name }}",
|
||||
response_type="text"
|
||||
)
|
||||
}
|
||||
)
|
||||
assert config.system_template == "Test system"
|
||||
assert len(config.prompts) == 1
|
||||
|
||||
def test_nested_template_includes(self, prompt_manager):
|
||||
"""Test templates with nested variable references"""
|
||||
# Add global terms
|
||||
prompt_manager.terms["company"] = "TrustGraph"
|
||||
prompt_manager.terms["year"] = 2024
|
||||
|
||||
# Add a nested template
|
||||
config = {
|
||||
"template.nested": json.dumps({
|
||||
"prompt": "{{ greeting }} from {{ company }} in {{ year }}!",
|
||||
"response-type": "text"
|
||||
})
|
||||
}
|
||||
prompt_manager.load_config({**prompt_manager.config.__dict__, **config})
|
||||
prompt_manager.prompts["nested"].terms = {"greeting": "Welcome"}
|
||||
|
||||
rendered = prompt_manager.render("nested", {"user": "Alice"})
|
||||
|
||||
# Should contain company and year from global terms
|
||||
assert "TrustGraph" in rendered
|
||||
assert "2024" in rendered
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_invocations(self, prompt_manager):
|
||||
"""Test concurrent prompt invocations"""
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.side_effect = [
|
||||
"Response for Alice",
|
||||
"Response for Bob",
|
||||
"Response for Charlie"
|
||||
]
|
||||
|
||||
# Simulate concurrent invocations
|
||||
import asyncio
|
||||
results = await asyncio.gather(
|
||||
prompt_manager.invoke("simple_text", {"name": "Alice"}, mock_llm),
|
||||
prompt_manager.invoke("simple_text", {"name": "Bob"}, mock_llm),
|
||||
prompt_manager.invoke("simple_text", {"name": "Charlie"}, mock_llm)
|
||||
)
|
||||
|
||||
assert len(results) == 3
|
||||
assert "Alice" in results[0]
|
||||
assert "Bob" in results[1]
|
||||
assert "Charlie" in results[2]
|
||||
|
||||
def test_empty_configuration(self):
|
||||
"""Test PromptManager with minimal configuration"""
|
||||
pm = PromptManager()
|
||||
pm.load_config({}) # Empty config
|
||||
|
||||
assert pm.config.system_template == "Be helpful." # Default system
|
||||
assert pm.terms == {} # Default empty terms
|
||||
assert len(pm.prompts) == 0
|
||||
428
tests/unit/test_prompt_manager_edge_cases.py
Normal file
428
tests/unit/test_prompt_manager_edge_cases.py
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
"""
|
||||
Edge case and error handling tests for PromptManager
|
||||
|
||||
These tests focus on boundary conditions, error scenarios, and
|
||||
unusual but valid use cases for the PromptManager.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from trustgraph.template.prompt_manager import PromptManager, PromptConfiguration, Prompt
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPromptManagerEdgeCases:
|
||||
"""Edge case tests for PromptManager"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_very_large_json_response(self):
|
||||
"""Test handling of very large JSON responses"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["large_json"]),
|
||||
"template.large_json": json.dumps({
|
||||
"prompt": "Generate large dataset",
|
||||
"response-type": "json"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
# Create a large JSON structure
|
||||
large_data = {
|
||||
f"item_{i}": {
|
||||
"name": f"Item {i}",
|
||||
"data": list(range(100)),
|
||||
"nested": {
|
||||
"level1": {
|
||||
"level2": f"Deep value {i}"
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in range(100)
|
||||
}
|
||||
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = json.dumps(large_data)
|
||||
|
||||
result = await pm.invoke("large_json", {}, mock_llm)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert len(result) == 100
|
||||
assert "item_50" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unicode_and_special_characters(self):
|
||||
"""Test handling of unicode and special characters"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["unicode"]),
|
||||
"template.unicode": json.dumps({
|
||||
"prompt": "Process text: {{ text }}",
|
||||
"response-type": "text"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
special_text = "Hello 世界! 🌍 Привет мир! مرحبا بالعالم"
|
||||
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = f"Processed: {special_text}"
|
||||
|
||||
result = await pm.invoke("unicode", {"text": special_text}, mock_llm)
|
||||
|
||||
assert special_text in result
|
||||
assert "🌍" in result
|
||||
assert "世界" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nested_json_in_text_response(self):
|
||||
"""Test text response containing JSON-like structures"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["text_with_json"]),
|
||||
"template.text_with_json": json.dumps({
|
||||
"prompt": "Explain this data",
|
||||
"response-type": "text" # Text response, not JSON
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = """
|
||||
The data structure is:
|
||||
{
|
||||
"key": "value",
|
||||
"nested": {
|
||||
"array": [1, 2, 3]
|
||||
}
|
||||
}
|
||||
This represents a nested object.
|
||||
"""
|
||||
|
||||
result = await pm.invoke("text_with_json", {}, mock_llm)
|
||||
|
||||
assert isinstance(result, str) # Should remain as text
|
||||
assert '"key": "value"' in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_json_blocks_in_response(self):
|
||||
"""Test response with multiple JSON blocks"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["multi_json"]),
|
||||
"template.multi_json": json.dumps({
|
||||
"prompt": "Generate examples",
|
||||
"response-type": "json"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = """
|
||||
Here's the first example:
|
||||
```json
|
||||
{"first": true, "value": 1}
|
||||
```
|
||||
|
||||
And here's another:
|
||||
```json
|
||||
{"second": true, "value": 2}
|
||||
```
|
||||
"""
|
||||
|
||||
# Should extract the first valid JSON block
|
||||
result = await pm.invoke("multi_json", {}, mock_llm)
|
||||
|
||||
assert result == {"first": True, "value": 1}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_json_with_comments(self):
|
||||
"""Test JSON response with comment-like content"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["json_comments"]),
|
||||
"template.json_comments": json.dumps({
|
||||
"prompt": "Generate config",
|
||||
"response-type": "json"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
mock_llm = AsyncMock()
|
||||
# JSON with comment-like content that should be extracted
|
||||
mock_llm.return_value = """
|
||||
// This is a configuration file
|
||||
{
|
||||
"setting": "value", // Important setting
|
||||
"number": 42
|
||||
}
|
||||
/* End of config */
|
||||
"""
|
||||
|
||||
# Standard JSON parser won't handle comments
|
||||
with pytest.raises(ValueError):
|
||||
await pm.invoke("json_comments", {}, mock_llm)
|
||||
|
||||
def test_template_with_raw_blocks(self):
|
||||
"""Test template with raw blocks that shouldn't be processed"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["raw_template"]),
|
||||
"template.raw_template": json.dumps({
|
||||
"prompt": """
|
||||
Normal: {{ variable }}
|
||||
{% raw %}
|
||||
Raw block: {{ this_should_not_be_processed }}
|
||||
{% endraw %}
|
||||
Normal again: {{ another }}
|
||||
""",
|
||||
"response-type": "text"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
result = pm.render(
|
||||
"raw_template",
|
||||
{"variable": "processed", "another": "also processed"}
|
||||
)
|
||||
|
||||
assert "processed" in result
|
||||
assert "{{ this_should_not_be_processed }}" in result # Should remain as-is
|
||||
assert "also processed" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_json_response_variations(self):
|
||||
"""Test various empty JSON response formats"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["empty_json"]),
|
||||
"template.empty_json": json.dumps({
|
||||
"prompt": "Generate empty data",
|
||||
"response-type": "json"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
empty_variations = [
|
||||
"{}",
|
||||
"[]",
|
||||
"null",
|
||||
'""',
|
||||
"0",
|
||||
"false"
|
||||
]
|
||||
|
||||
for empty_value in empty_variations:
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = empty_value
|
||||
|
||||
result = await pm.invoke("empty_json", {}, mock_llm)
|
||||
assert result == json.loads(empty_value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_malformed_json_recovery(self):
|
||||
"""Test recovery from slightly malformed JSON"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["malformed"]),
|
||||
"template.malformed": json.dumps({
|
||||
"prompt": "Generate data",
|
||||
"response-type": "json"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
# Missing closing brace - should fail
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = '{"key": "value"'
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await pm.invoke("malformed", {}, mock_llm)
|
||||
|
||||
assert "JSON parse fail" in str(exc_info.value)
|
||||
|
||||
def test_template_infinite_loop_protection(self):
|
||||
"""Test protection against infinite template loops"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["recursive"]),
|
||||
"template.recursive": json.dumps({
|
||||
"prompt": "{{ recursive_var }}",
|
||||
"response-type": "text"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
pm.prompts["recursive"].terms = {"recursive_var": "This includes {{ recursive_var }}"}
|
||||
|
||||
# This should not cause infinite recursion
|
||||
result = pm.render("recursive", {})
|
||||
|
||||
# The exact behavior depends on the template engine
|
||||
assert isinstance(result, str)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extremely_long_template(self):
|
||||
"""Test handling of extremely long templates"""
|
||||
# Create a very long template
|
||||
long_template = "Start\n" + "\n".join([
|
||||
f"Line {i}: " + "{{ var_" + str(i) + " }}"
|
||||
for i in range(1000)
|
||||
]) + "\nEnd"
|
||||
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["long"]),
|
||||
"template.long": json.dumps({
|
||||
"prompt": long_template,
|
||||
"response-type": "text"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
# Create corresponding variables
|
||||
variables = {f"var_{i}": f"value_{i}" for i in range(1000)}
|
||||
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.return_value = "Processed long template"
|
||||
|
||||
result = await pm.invoke("long", variables, mock_llm)
|
||||
|
||||
assert result == "Processed long template"
|
||||
|
||||
# Check that template was rendered correctly
|
||||
call_args = mock_llm.call_args[1]
|
||||
rendered = call_args["prompt"]
|
||||
assert "Line 500: value_500" in rendered
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_json_schema_with_additional_properties(self):
|
||||
"""Test JSON schema validation with additional properties"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["strict_schema"]),
|
||||
"template.strict_schema": json.dumps({
|
||||
"prompt": "Generate user",
|
||||
"response-type": "json",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": False
|
||||
}
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
mock_llm = AsyncMock()
|
||||
# Response with extra property
|
||||
mock_llm.return_value = '{"name": "John", "age": 30}'
|
||||
|
||||
# Should fail validation due to additionalProperties: false
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await pm.invoke("strict_schema", {}, mock_llm)
|
||||
|
||||
assert "Schema validation fail" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_timeout_handling(self):
|
||||
"""Test handling of LLM timeouts"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["timeout_test"]),
|
||||
"template.timeout_test": json.dumps({
|
||||
"prompt": "Test prompt",
|
||||
"response-type": "text"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.side_effect = asyncio.TimeoutError("LLM request timed out")
|
||||
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
await pm.invoke("timeout_test", {}, mock_llm)
|
||||
|
||||
def test_template_with_filters_and_tests(self):
|
||||
"""Test template with Jinja2 filters and tests"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["filters"]),
|
||||
"template.filters": json.dumps({
|
||||
"prompt": """
|
||||
{% if items %}
|
||||
Items ({{ items|length }}):
|
||||
{% for item in items|sort %}
|
||||
- {{ item|upper }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
No items
|
||||
{% endif %}
|
||||
""",
|
||||
"response-type": "text"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
# Test with items
|
||||
result = pm.render(
|
||||
"filters",
|
||||
{"items": ["banana", "apple", "cherry"]}
|
||||
)
|
||||
|
||||
assert "Items (3)" in result
|
||||
assert "- APPLE" in result
|
||||
assert "- BANANA" in result
|
||||
assert "- CHERRY" in result
|
||||
|
||||
# Test without items
|
||||
result = pm.render("filters", {"items": []})
|
||||
assert "No items" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_template_modifications(self):
|
||||
"""Test thread safety of template operations"""
|
||||
pm = PromptManager()
|
||||
config = {
|
||||
"system": json.dumps("Test"),
|
||||
"template-index": json.dumps(["concurrent"]),
|
||||
"template.concurrent": json.dumps({
|
||||
"prompt": "User: {{ user }}",
|
||||
"response-type": "text"
|
||||
})
|
||||
}
|
||||
pm.load_config(config)
|
||||
|
||||
mock_llm = AsyncMock()
|
||||
mock_llm.side_effect = lambda **kwargs: f"Response for {kwargs['prompt'].split()[1]}"
|
||||
|
||||
# Simulate concurrent invocations with different users
|
||||
import asyncio
|
||||
tasks = []
|
||||
for i in range(10):
|
||||
tasks.append(
|
||||
pm.invoke("concurrent", {"user": f"User{i}"}, mock_llm)
|
||||
)
|
||||
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
# Each result should correspond to its user
|
||||
for i, result in enumerate(results):
|
||||
assert f"User{i}" in result
|
||||
Loading…
Add table
Add a link
Reference in a new issue