""" BQAS Test Fixtures """ import os import pytest import pytest_asyncio import yaml from pathlib import Path from typing import List, Dict, Any import httpx # Add parent to path for imports import sys sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from bqas.judge import LLMJudge from bqas.rag_judge import RAGJudge from bqas.config import BQASConfig from bqas.regression_tracker import RegressionTracker from bqas.synthetic_generator import SyntheticGenerator from bqas.backlog_generator import BacklogGenerator @pytest.fixture(scope="session") def bqas_config(): """BQAS configuration for tests.""" return BQASConfig( ollama_base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434"), judge_model=os.getenv("BQAS_JUDGE_MODEL", "qwen2.5:32b"), voice_service_url=os.getenv("VOICE_SERVICE_URL", "http://localhost:8091"), db_path=os.getenv("BQAS_DB_PATH", "bqas_test_history.db"), ) @pytest.fixture(scope="session") def llm_judge(bqas_config): """LLM Judge instance.""" return LLMJudge(config=bqas_config) @pytest.fixture(scope="session") def rag_judge(bqas_config): """RAG Judge instance for RAG/Correction tests.""" return RAGJudge(config=bqas_config) @pytest.fixture(scope="session") def regression_tracker(bqas_config): """Regression tracker instance.""" return RegressionTracker(config=bqas_config) @pytest.fixture(scope="session") def synthetic_generator(bqas_config): """Synthetic test generator instance.""" return SyntheticGenerator(config=bqas_config) @pytest.fixture(scope="session") def backlog_generator(bqas_config): """Backlog generator instance.""" return BacklogGenerator(config=bqas_config) @pytest_asyncio.fixture async def voice_service_client(bqas_config): """Async HTTP client for voice service.""" async with httpx.AsyncClient( base_url=bqas_config.voice_service_url, timeout=30.0, ) as client: yield client def load_golden_tests_from_file(yaml_path: Path) -> List[Dict[str, Any]]: """Load test cases from a YAML file.""" with open(yaml_path, 'r', encoding='utf-8') as f: data = yaml.safe_load(f) tests = [] # Handle different YAML structures if 'tests' in data: tests.extend(data['tests']) if 'edge_cases' in data: tests.extend(data['edge_cases']) if 'workflow_tests' in data: # Flatten workflow tests - take first step for wf in data['workflow_tests']: if 'steps' in wf and wf['steps']: first_step = wf['steps'][0] tests.append({ 'id': wf.get('id', 'WF-XXX'), 'name': wf.get('name', 'Workflow'), 'input': first_step.get('input', ''), 'expected_intent': first_step.get('expected_intent', 'unknown'), 'min_score': 3.0, }) return tests @pytest.fixture(scope="session") def golden_tests() -> List[Dict[str, Any]]: """Load all golden tests from YAML files.""" golden_dir = Path(__file__).parent / "golden_tests" all_tests = [] for yaml_file in golden_dir.glob("*.yaml"): tests = load_golden_tests_from_file(yaml_file) all_tests.extend(tests) return all_tests @pytest.fixture(scope="session") def intent_tests() -> List[Dict[str, Any]]: """Load only intent tests.""" yaml_path = Path(__file__).parent / "golden_tests" / "intent_tests.yaml" return load_golden_tests_from_file(yaml_path) @pytest.fixture(scope="session") def edge_case_tests() -> List[Dict[str, Any]]: """Load only edge case tests.""" yaml_path = Path(__file__).parent / "golden_tests" / "edge_cases.yaml" return load_golden_tests_from_file(yaml_path) def load_rag_tests_from_file(yaml_path: Path) -> List[Dict[str, Any]]: """Load RAG test cases from a YAML file with multiple documents.""" with open(yaml_path, 'r', encoding='utf-8') as f: content = f.read() tests = [] # Handle YAML with multiple documents (separated by ---) documents = list(yaml.safe_load_all(content)) for doc in documents: if doc and 'tests' in doc: tests.extend(doc['tests']) if doc and 'edge_cases' in doc: tests.extend(doc['edge_cases']) return tests @pytest.fixture(scope="session") def rag_tests() -> List[Dict[str, Any]]: """Load RAG/Correction tests from golden suite.""" yaml_path = Path(__file__).parent / "golden_tests" / "golden_rag_correction_v1.yaml" if yaml_path.exists(): return load_rag_tests_from_file(yaml_path) return [] @pytest.fixture(scope="session") def rag_retrieval_tests(rag_tests) -> List[Dict[str, Any]]: """Load only EH retrieval tests.""" return [t for t in rag_tests if t.get("category") == "eh_retrieval"] @pytest.fixture(scope="session") def rag_operator_tests(rag_tests) -> List[Dict[str, Any]]: """Load only operator alignment tests.""" return [t for t in rag_tests if t.get("category") == "operator_alignment"] @pytest.fixture(scope="session") def rag_privacy_tests(rag_tests) -> List[Dict[str, Any]]: """Load only privacy compliance tests.""" return [t for t in rag_tests if t.get("category") == "privacy_compliance"] @pytest.fixture def sample_test_result(): """Sample test result for testing.""" from datetime import datetime, timezone from bqas.metrics import TestResult return TestResult( test_id="TEST-001", test_name="Sample Test", user_input="Notiz zu Max: heute gestoert", expected_intent="student_observation", detected_intent="student_observation", response="Notiz gespeichert", intent_accuracy=100, faithfulness=5, relevance=5, coherence=5, safety="pass", composite_score=4.8, passed=True, reasoning="Perfect match", timestamp=datetime.now(timezone.utc), duration_ms=1500, )