Files

111 lines
4.2 KiB
Python

import pytest
import asyncio
from unittest.mock import AsyncMock, patch
from services.providers.base import BaseProviderAdapter, ProviderResponse
from services.providers.registry import ProviderRegistry, ProviderCapability
print(f"DEBUG: BaseProviderAdapter abstract methods: {BaseProviderAdapter.__abstractmethods__}")
# 1. Mock adapter inheriting from BaseProviderAdapter
class MockR19Adapter(BaseProviderAdapter):
async def execute(self, action: str, params: dict) -> ProviderResponse:
# Simulate various responses
if action == "query":
raw_content = params.get("mock_content", "")
# R19: Enforce cleaning in the adapter implementation
cleaned_content = self.clean_llm_output(raw_content)
return ProviderResponse(
success=True,
data={"content": cleaned_content},
meta={"usage": 100}
)
elif action == "fallible":
raise RuntimeError("Simulated failure")
return ProviderResponse(success=False, error="Unknown action")
async def health_check(self) -> bool:
return True
async def _submit_enrichment_impl(self, payload: dict) -> ProviderResponse:
# Internal implementation receives already-sanitized payload
return await self.execute("submit", payload)
# 2. Test Fixtures
@pytest.fixture
def adapter():
return MockR19Adapter(provider_id="mock_r19")
@pytest.fixture
def anyio_backend():
return 'asyncio'
@pytest.fixture
def mock_registry():
ProviderRegistry._adapters.clear()
ProviderRegistry._capabilities.clear()
yield ProviderRegistry
ProviderRegistry._adapters.clear()
ProviderRegistry._capabilities.clear()
# 3. R19 Regression Tests
@pytest.mark.anyio
async def test_r19_output_contract_normalization(adapter):
"""
R19: Ensure output is normalized to ProviderResponse.
"""
# Happy path
response = await adapter.execute_with_retry(adapter.execute, "query", {"mock_content": "Valid output"})
assert response.success is True
assert response.data["content"] == "Valid output"
assert "usage" in response.meta
@pytest.mark.anyio
async def test_r19_truncation_simulated(adapter):
"""
R19: Verify large keys/values don't crash adapter (shim for truncation).
Real truncation happens in outbound funnel, but adapter must handle large payloads.
"""
large_input = "A" * 100000
response = await adapter.execute_with_retry(adapter.execute, "query", {"mock_content": large_input})
assert response.success is True
assert len(response.data["content"]) == 100000
# Note: Full truncation downstream is covered by outbound tests,
# here we ensure adapter doesn't choke on large data.
@pytest.mark.anyio
async def test_r19_error_fallback(adapter):
"""
R19: Ensure exceptions are caught and wrapped in ProviderResponse(success=False).
"""
response = await adapter.execute_with_retry(adapter.execute, "fallible", {})
assert response.success is False
assert "Simulated failure" in response.error
@pytest.mark.anyio
async def test_r19_hidden_marker_stripping(adapter):
"""
R19: Check if markers/thinking tags are STRIPPED from output (not preserved).
Roadmap R19 requires stripping hidden reasoning blocks before render/store.
"""
thinking_content = "<think>Wait...</think>Result"
response = await adapter.execute_with_retry(adapter.execute, "query", {"mock_content": thinking_content})
assert response.success is True
assert "<think>" not in response.data["content"]
assert "</think>" not in response.data["content"]
assert response.data["content"] == "Result"
@pytest.mark.anyio
async def test_r19_malformed_json_resilience(adapter):
"""
R19: Ensure malformed JSON in provider response (simulated) is handled gracefully.
"""
# For a base adapter, we handle the error string.
# Specific providers would parse JSON, but base catches the exception.
with patch.object(MockR19Adapter, 'execute', side_effect=ValueError("Expecting value: line 1 column 1 (char 0)")):
response = await adapter.execute_with_retry(adapter.execute, "query", {})
assert response.success is False
assert "Expecting value" in response.error