Download tests/api/test_ab_testing_routes.py from techprotrade/annator-command-center: direct link, hf CLI and curl.
- Browser
- Download file 52.9 kB
-
https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/api/test_ab_testing_routes.py
- Command line
-
hf download hf://spaces/techprotrade/annator-command-center/tests/api/test_ab_testing_routes.py
-
curl -L -o test_ab_testing_routes.py https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/api/test_ab_testing_routes.py
52.9 kB
| """ | |
| A/B Testing Routes Test Suite | |
| Comprehensive TestClient-based testing for ab_testing.py API routes. | |
| Tests cover all endpoints with proper mocking. | |
| """ | |
| import pytest | |
| from datetime import datetime | |
| from fastapi import FastAPI | |
| from unittest.mock import MagicMock, patch | |
| from sqlalchemy.orm import Session | |
| from api.ab_testing import router | |
| # ============================================================================ | |
| # TestCreateTest - 8 tests | |
| # ============================================================================ | |
| class TestCreateTest: | |
| """Test suite for POST /api/ab-tests/create endpoint.""" | |
| def client(self): | |
| """Create TestClient with A/B testing router.""" | |
| app = FastAPI() | |
| app.include_router(router) # Router already has prefix | |
| from fastapi.testclient import TestClient | |
| return TestClient(app) | |
| def test_create_test_success(self, client): | |
| """Test creating A/B test with valid data returns 200.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "draft", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a": {"name": "Control", "config": {"temperature": 0.7}}, | |
| "variant_b": {"name": "Treatment", "config": {"temperature": 0.9}}, | |
| "primary_metric": "satisfaction_rate", | |
| "min_sample_size": 100, | |
| "traffic_percentage": 0.5 | |
| } | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Test A", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a_config": {"temperature": 0.7}, | |
| "variant_b_config": {"temperature": 0.9}, | |
| "primary_metric": "satisfaction_rate" | |
| }) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["success"] is True | |
| assert data["data"]["test_id"] == "test-123" | |
| def test_create_test_with_all_fields(self, client): | |
| """Test creating test with all optional fields included.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "test_id": "test-456", | |
| "name": "Comprehensive Test", | |
| "status": "draft", | |
| "test_type": "agent_config", | |
| "agent_id": "agent-2", | |
| "variant_a": {"name": "V1", "config": {"model": "gpt-3"}}, | |
| "variant_b": {"name": "V2", "config": {"model": "gpt-4"}}, | |
| "primary_metric": "success_rate", | |
| "min_sample_size": 200, | |
| "traffic_percentage": 0.6 | |
| } | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Comprehensive Test", | |
| "test_type": "agent_config", | |
| "agent_id": "agent-2", | |
| "variant_a_config": {"model": "gpt-3"}, | |
| "variant_b_config": {"model": "gpt-4"}, | |
| "primary_metric": "success_rate", | |
| "description": "Testing model performance", | |
| "min_sample_size": 200, | |
| "traffic_percentage": 0.6, | |
| "confidence_level": 0.99, | |
| "secondary_metrics": ["response_time", "error_rate"] | |
| }) | |
| assert response.status_code == 200 | |
| def test_create_test_default_values(self, client): | |
| """Test creating test uses correct defaults for optional fields.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "test_id": "test-defaults", | |
| "name": "Default Test", | |
| "status": "draft", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a": {"name": "Control", "config": {}}, | |
| "variant_b": {"name": "Treatment", "config": {}}, | |
| "primary_metric": "satisfaction_rate", | |
| "min_sample_size": 100, | |
| "traffic_percentage": 0.5 | |
| } | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Default Test", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a_config": {}, | |
| "variant_b_config": {}, | |
| "primary_metric": "satisfaction_rate" | |
| }) | |
| assert response.status_code == 200 | |
| def test_create_test_prompt_type(self, client): | |
| """Test creating test with test_type='prompt' validates correctly.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "test_id": "test-prompt", | |
| "name": "Prompt Test", | |
| "status": "draft", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a": {"name": "Control", "config": {"prompt": "v1"}}, | |
| "variant_b": {"name": "Treatment", "config": {"prompt": "v2"}}, | |
| "primary_metric": "satisfaction_rate", | |
| "min_sample_size": 100, | |
| "traffic_percentage": 0.5 | |
| } | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Prompt Test", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a_config": {"prompt": "v1"}, | |
| "variant_b_config": {"prompt": "v2"}, | |
| "primary_metric": "satisfaction_rate" | |
| }) | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["test_type"] == "prompt" | |
| def test_create_test_agent_config_type(self, client): | |
| """Test creating test with test_type='agent_config' validates correctly.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "test_id": "test-config", | |
| "name": "Config Test", | |
| "status": "draft", | |
| "test_type": "agent_config", | |
| "agent_id": "agent-1", | |
| "variant_a": {"name": "Control", "config": {"temperature": 0.5}}, | |
| "variant_b": {"name": "Treatment", "config": {"temperature": 0.9}}, | |
| "primary_metric": "success_rate", | |
| "min_sample_size": 100, | |
| "traffic_percentage": 0.5 | |
| } | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Config Test", | |
| "test_type": "agent_config", | |
| "agent_id": "agent-1", | |
| "variant_a_config": {"temperature": 0.5}, | |
| "variant_b_config": {"temperature": 0.9}, | |
| "primary_metric": "success_rate" | |
| }) | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["test_type"] == "agent_config" | |
| def test_create_test_validation_error(self, client): | |
| """Test creating test with invalid data returns 400.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "error": "Invalid test configuration" | |
| } | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Invalid Test", | |
| "test_type": "invalid_type", | |
| "agent_id": "", | |
| "variant_a_config": {}, | |
| "variant_b_config": {}, | |
| "primary_metric": "" | |
| }) | |
| assert response.status_code == 400 | |
| data = response.json() | |
| # HTTPException wraps response in "detail" field | |
| assert "detail" in data | |
| assert data["detail"]["success"] is False | |
| def test_create_test_traffic_percentage_validation(self, client): | |
| """Test traffic_percentage enforces ge=0.0 and le=1.0 constraints.""" | |
| # Pydantic validation happens before service call, returns 422 | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Test", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a_config": {}, | |
| "variant_b_config": {}, | |
| "primary_metric": "satisfaction_rate", | |
| "traffic_percentage": 1.5 | |
| }) | |
| assert response.status_code == 422 | |
| def test_create_test_confidence_validation(self, client): | |
| """Test confidence_level enforces ge=0.0 and le=1.0 constraints.""" | |
| # Pydantic validation happens before service call, returns 422 | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Test", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a_config": {}, | |
| "variant_b_config": {}, | |
| "primary_metric": "satisfaction_rate", | |
| "confidence_level": 1.5 | |
| }) | |
| assert response.status_code == 422 | |
| # ============================================================================ | |
| # TestStartTest - 5 tests | |
| # ============================================================================ | |
| class TestStartTest: | |
| """Test suite for POST /api/ab-tests/{test_id}/start endpoint.""" | |
| def client(self): | |
| """Create TestClient with A/B testing router.""" | |
| app = FastAPI() | |
| app.include_router(router) # Router already has prefix | |
| from fastapi.testclient import TestClient | |
| return TestClient(app) | |
| def test_start_test_success(self, client): | |
| """Test starting test changes status to running.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.start_test.return_value = { | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "running", | |
| "started_at": "2026-03-12T10:00:00Z" | |
| } | |
| response = client.post("/api/ab-tests/test-123/start") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["success"] is True | |
| assert data["data"]["status"] == "running" | |
| def test_start_test_includes_timestamp(self, client): | |
| """Test starting test returns started_at timestamp.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| started_at = "2026-03-12T10:30:00Z" | |
| mock_service.start_test.return_value = { | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "running", | |
| "started_at": started_at | |
| } | |
| response = client.post("/api/ab-tests/test-123/start") | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["started_at"] == started_at | |
| def test_start_test_already_running(self, client): | |
| """Test starting already running test returns error.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.start_test.return_value = { | |
| "error": "Test must be in 'draft' status to start, current status: running" | |
| } | |
| response = client.post("/api/ab-tests/test-123/start") | |
| assert response.status_code == 400 | |
| def test_start_test_not_found(self, client): | |
| """Test starting non-existent test returns 404.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.start_test.return_value = { | |
| "error": "Test 'nonexistent' not found" | |
| } | |
| response = client.post("/api/ab-tests/nonexistent/start") | |
| assert response.status_code == 400 | |
| def test_start_test_error_handling(self, client): | |
| """Test service exceptions are handled gracefully.""" | |
| # Note: The endpoint doesn't have try-except blocks, so exceptions | |
| # will propagate. In production, FastAPI's default exception handler | |
| # would return 500, but in TestClient, the exception propagates. | |
| # This test documents the current behavior - exceptions are not caught. | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| # When service returns an error dict, endpoint raises HTTPException 400 | |
| mock_service.start_test.return_value = { | |
| "error": "Database error" | |
| } | |
| response = client.post("/api/ab-tests/test-123/start") | |
| # Service error returns 400, not 500 | |
| assert response.status_code == 400 | |
| # ============================================================================ | |
| # TestCompleteTest - 6 tests | |
| # ============================================================================ | |
| class TestCompleteTest: | |
| """Test suite for POST /api/ab-tests/{test_id}/complete endpoint.""" | |
| def client(self): | |
| """Create TestClient with A/B testing router.""" | |
| app = FastAPI() | |
| app.include_router(router) # Router already has prefix | |
| from fastapi.testclient import TestClient | |
| return TestClient(app) | |
| def test_complete_test_success(self, client): | |
| """Test completing test calculates results and changes status.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.complete_test.return_value = { | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "completed", | |
| "completed_at": "2026-03-12T12:00:00Z", | |
| "variant_a_metrics": { | |
| "count": 150, | |
| "success_count": 120, | |
| "success_rate": 0.80 | |
| }, | |
| "variant_b_metrics": { | |
| "count": 150, | |
| "success_count": 135, | |
| "success_rate": 0.90 | |
| }, | |
| "p_value": 0.02, | |
| "winner": "B" | |
| } | |
| response = client.post("/api/ab-tests/test-123/complete") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["success"] is True | |
| assert data["data"]["status"] == "completed" | |
| def test_complete_test_includes_results(self, client): | |
| """Test completing test returns variant_a_metrics and variant_b_metrics.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.complete_test.return_value = { | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "completed", | |
| "completed_at": "2026-03-12T12:00:00Z", | |
| "variant_a_metrics": {"count": 100, "success_rate": 0.80}, | |
| "variant_b_metrics": {"count": 100, "success_rate": 0.90}, | |
| "p_value": 0.05, | |
| "winner": "B" | |
| } | |
| response = client.post("/api/ab-tests/test-123/complete") | |
| assert response.status_code == 200 | |
| data = response.json()["data"] | |
| assert data["variant_a_metrics"]["success_rate"] == 0.80 | |
| def test_complete_test_includes_winner(self, client): | |
| """Test completing test returns winner (A, B, or inconclusive).""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.complete_test.return_value = { | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "completed", | |
| "completed_at": "2026-03-12T12:00:00Z", | |
| "variant_a_metrics": {}, | |
| "variant_b_metrics": {}, | |
| "p_value": 0.01, | |
| "winner": "B" | |
| } | |
| response = client.post("/api/ab-tests/test-123/complete") | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["winner"] == "B" | |
| def test_complete_test_includes_p_value(self, client): | |
| """Test completing test returns statistical significance p_value.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.complete_test.return_value = { | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "completed", | |
| "completed_at": "2026-03-12T12:00:00Z", | |
| "variant_a_metrics": {}, | |
| "variant_b_metrics": {}, | |
| "p_value": 0.02, | |
| "winner": "A" | |
| } | |
| response = client.post("/api/ab-tests/test-123/complete") | |
| assert response.status_code == 200 | |
| assert "p_value" in response.json()["data"] | |
| def test_complete_test_insufficient_data(self, client): | |
| """Test completing test with insufficient data returns error.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.complete_test.return_value = { | |
| "error": "Insufficient sample size to complete test." | |
| } | |
| response = client.post("/api/ab-tests/test-123/complete") | |
| assert response.status_code == 400 | |
| def test_complete_test_not_found(self, client): | |
| """Test completing non-existent test returns 404.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.complete_test.return_value = { | |
| "error": "Test 'nonexistent' not found" | |
| } | |
| response = client.post("/api/ab-tests/nonexistent/complete") | |
| assert response.status_code == 400 | |
| # ============================================================================ | |
| # TestAssignVariant - 7 tests | |
| # ============================================================================ | |
| class TestAssignVariant: | |
| """Test suite for POST /api/ab-tests/{test_id}/assign endpoint.""" | |
| def client(self): | |
| """Create TestClient with A/B testing router.""" | |
| app = FastAPI() | |
| app.include_router(router) # Router already has prefix | |
| from fastapi.testclient import TestClient | |
| return TestClient(app) | |
| def test_assign_variant_success(self, client): | |
| """Test assigning user to variant returns variant and config.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.assign_variant.return_value = { | |
| "test_id": "test-123", | |
| "user_id": "user-1", | |
| "variant": "A", | |
| "variant_name": "Control", | |
| "config": {"temperature": 0.7}, | |
| "existing_assignment": False | |
| } | |
| response = client.post("/api/ab-tests/test-123/assign", json={ | |
| "user_id": "user-1" | |
| }) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["success"] is True | |
| assert data["data"]["variant"] in ["A", "B"] | |
| def test_assign_variant_consistent(self, client): | |
| """Test same user gets same variant consistently.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| # First call | |
| mock_service.assign_variant.return_value = { | |
| "test_id": "test-123", | |
| "user_id": "user-1", | |
| "variant": "A", | |
| "variant_name": "Control", | |
| "config": {}, | |
| "existing_assignment": False | |
| } | |
| response1 = client.post("/api/ab-tests/test-123/assign", json={"user_id": "user-1"}) | |
| variant1 = response1.json()["data"]["variant"] | |
| # Second call should return same variant | |
| mock_service.assign_variant.return_value = { | |
| "test_id": "test-123", | |
| "user_id": "user-1", | |
| "variant": variant1, | |
| "variant_name": "Control", | |
| "config": {}, | |
| "existing_assignment": True | |
| } | |
| response2 = client.post("/api/ab-tests/test-123/assign", json={"user_id": "user-1"}) | |
| variant2 = response2.json()["data"]["variant"] | |
| assert variant1 == variant2 | |
| def test_assign_variant_returns_config(self, client): | |
| """Test assigning variant returns variant configuration.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.assign_variant.return_value = { | |
| "test_id": "test-123", | |
| "user_id": "user-1", | |
| "variant": "A", | |
| "variant_name": "Control", | |
| "config": {"temperature": 0.7, "max_tokens": 1000}, | |
| "existing_assignment": False | |
| } | |
| response = client.post("/api/ab-tests/test-123/assign", json={"user_id": "user-1"}) | |
| assert response.status_code == 200 | |
| config = response.json()["data"]["config"] | |
| assert config["temperature"] == 0.7 | |
| def test_assign_variant_existing_assignment(self, client): | |
| """Test reassigning user indicates existing_assignment=True.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.assign_variant.return_value = { | |
| "test_id": "test-123", | |
| "user_id": "user-1", | |
| "variant": "A", | |
| "variant_name": "Control", | |
| "config": {}, | |
| "existing_assignment": True | |
| } | |
| response = client.post("/api/ab-tests/test-123/assign", json={"user_id": "user-1"}) | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["existing_assignment"] is True | |
| def test_assign_variant_with_session(self, client): | |
| """Test assigning variant handles optional session_id.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.assign_variant.return_value = { | |
| "test_id": "test-123", | |
| "user_id": "user-1", | |
| "variant": "B", | |
| "variant_name": "Treatment", | |
| "config": {}, | |
| "existing_assignment": False | |
| } | |
| response = client.post("/api/ab-tests/test-123/assign", json={ | |
| "user_id": "user-1", | |
| "session_id": "session-123" | |
| }) | |
| assert response.status_code == 200 | |
| def test_assign_variant_not_found(self, client): | |
| """Test assigning to non-existent test returns 400.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.assign_variant.return_value = { | |
| "error": "Test 'nonexistent' not found" | |
| } | |
| response = client.post("/api/ab-tests/nonexistent/assign", json={ | |
| "user_id": "user-1" | |
| }) | |
| assert response.status_code == 400 | |
| def test_assign_variant_error_handling(self, client): | |
| """Test service exceptions are handled gracefully.""" | |
| # Note: The endpoint doesn't have try-except blocks, so exceptions | |
| # will propagate. This test documents the current behavior. | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| # When service returns an error dict, endpoint raises HTTPException 400 | |
| mock_service.assign_variant.return_value = { | |
| "error": "Service error" | |
| } | |
| response = client.post("/api/ab-tests/test-123/assign", json={"user_id": "user-1"}) | |
| # Service error returns 400, not 500 | |
| assert response.status_code == 400 | |
| # ============================================================================ | |
| # TestRecordMetric - 6 tests | |
| # ============================================================================ | |
| class TestRecordMetric: | |
| """Test suite for POST /api/ab-tests/{test_id}/record endpoint.""" | |
| def client(self): | |
| """Create TestClient with A/B testing router.""" | |
| app = FastAPI() | |
| app.include_router(router) # Router already has prefix | |
| from fastapi.testclient import TestClient | |
| return TestClient(app) | |
| def test_record_metric_with_success(self, client): | |
| """Test recording boolean success metric.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.record_metric.return_value = { | |
| "test_id": "test-123", | |
| "user_id": "user-1", | |
| "variant": "A", | |
| "success": True, | |
| "metric_value": None, | |
| "recorded_at": "2026-03-12T11:00:00Z" | |
| } | |
| response = client.post("/api/ab-tests/test-123/record", json={ | |
| "user_id": "user-1", | |
| "success": True | |
| }) | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["success"] is True | |
| def test_record_metric_with_value(self, client): | |
| """Test recording numerical metric value.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.record_metric.return_value = { | |
| "test_id": "test-123", | |
| "user_id": "user-1", | |
| "variant": "A", | |
| "success": None, | |
| "metric_value": 4.5, | |
| "recorded_at": "2026-03-12T11:00:00Z" | |
| } | |
| response = client.post("/api/ab-tests/test-123/record", json={ | |
| "user_id": "user-1", | |
| "metric_value": 4.5 | |
| }) | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["metric_value"] == 4.5 | |
| def test_record_metric_with_metadata(self, client): | |
| """Test recording metric includes metadata.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.record_metric.return_value = { | |
| "test_id": "test-123", | |
| "user_id": "user-1", | |
| "variant": "A", | |
| "success": True, | |
| "metric_value": None, | |
| "recorded_at": "2026-03-12T11:00:00Z" | |
| } | |
| response = client.post("/api/ab-tests/test-123/record", json={ | |
| "user_id": "user-1", | |
| "success": True, | |
| "metadata": {"source": "mobile", "version": "1.0"} | |
| }) | |
| assert response.status_code == 200 | |
| def test_record_metric_minimal(self, client): | |
| """Test recording with only required user_id field.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.record_metric.return_value = { | |
| "test_id": "test-123", | |
| "user_id": "user-1", | |
| "variant": "A", | |
| "success": None, | |
| "metric_value": None, | |
| "recorded_at": "2026-03-12T11:00:00Z" | |
| } | |
| response = client.post("/api/ab-tests/test-123/record", json={"user_id": "user-1"}) | |
| assert response.status_code == 200 | |
| def test_record_metric_validation(self, client): | |
| """Test recording with invalid data returns error.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.record_metric.return_value = { | |
| "error": "Invalid metric data" | |
| } | |
| response = client.post("/api/ab-tests/test-123/record", json={ | |
| "user_id": "user-1", | |
| "success": None, | |
| "metric_value": None | |
| }) | |
| assert response.status_code == 400 | |
| def test_record_metric_not_found(self, client): | |
| """Test recording for non-existent participant returns 400.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.record_metric.return_value = { | |
| "error": "Participant not found" | |
| } | |
| response = client.post("/api/ab-tests/test-123/record", json={ | |
| "user_id": "user-999", | |
| "success": True | |
| }) | |
| assert response.status_code == 400 | |
| # ============================================================================ | |
| # TestGetTestResults - 5 tests | |
| # ============================================================================ | |
| class TestGetTestResults: | |
| """Test suite for GET /api/ab-tests/{test_id}/results endpoint.""" | |
| def client(self): | |
| """Create TestClient with A/B testing router.""" | |
| app = FastAPI() | |
| app.include_router(router) # Router already has prefix | |
| from fastapi.testclient import TestClient | |
| return TestClient(app) | |
| def test_get_test_results_success(self, client): | |
| """Test getting test results returns metrics data.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.get_test_results.return_value = { | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "completed", | |
| "variant_a": { | |
| "name": "Control", | |
| "participant_count": 150, | |
| "metrics": {"count": 150, "success_rate": 0.80} | |
| }, | |
| "variant_b": { | |
| "name": "Treatment", | |
| "participant_count": 150, | |
| "metrics": {"count": 150, "success_rate": 0.90} | |
| }, | |
| "winner": "B", | |
| "statistical_significance": 0.02 | |
| } | |
| response = client.get("/api/ab-tests/test-123/results") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert "variant_a" in data["data"] | |
| assert "variant_b" in data["data"] | |
| def test_get_test_results_includes_variants(self, client): | |
| """Test getting results returns variant_a and variant_b data.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.get_test_results.return_value = { | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "running", | |
| "variant_a": { | |
| "name": "Control", | |
| "participant_count": 100, | |
| "metrics": {"count": 100} | |
| }, | |
| "variant_b": { | |
| "name": "Treatment", | |
| "participant_count": 100, | |
| "metrics": {"count": 100} | |
| }, | |
| "winner": None | |
| } | |
| response = client.get("/api/ab-tests/test-123/results") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["data"]["variant_a"]["participant_count"] == 100 | |
| def test_get_test_results_winner(self, client): | |
| """Test getting results includes winner when completed.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.get_test_results.return_value = { | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "completed", | |
| "variant_a": {"name": "Control", "participant_count": 150, "metrics": {}}, | |
| "variant_b": {"name": "Treatment", "participant_count": 150, "metrics": {}}, | |
| "winner": "B", | |
| "statistical_significance": 0.01 | |
| } | |
| response = client.get("/api/ab-tests/test-123/results") | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["winner"] == "B" | |
| def test_get_test_results_not_found(self, client): | |
| """Test getting results for non-existent test returns 404.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.get_test_results.return_value = { | |
| "error": "Test 'nonexistent' not found" | |
| } | |
| response = client.get("/api/ab-tests/nonexistent/results") | |
| assert response.status_code == 404 | |
| def test_get_test_results_error_handling(self, client): | |
| """Test service exceptions are handled gracefully.""" | |
| # Note: The endpoint doesn't have try-except blocks, so exceptions | |
| # will propagate. This test documents the current behavior. | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| # When service returns an error dict, endpoint raises HTTPException 404 | |
| mock_service.get_test_results.return_value = { | |
| "error": "Database error" | |
| } | |
| response = client.get("/api/ab-tests/test-123/results") | |
| # Service error returns 404 (not_found_error), not 500 | |
| assert response.status_code == 404 | |
| # ============================================================================ | |
| # TestListTests - 6 tests | |
| # ============================================================================ | |
| class TestListTests: | |
| """Test suite for GET /api/ab-tests endpoint.""" | |
| def client(self): | |
| """Create TestClient with A/B testing router.""" | |
| app = FastAPI() | |
| app.include_router(router) # Router already has prefix | |
| from fastapi.testclient import TestClient | |
| return TestClient(app) | |
| def test_list_tests_success(self, client): | |
| """Test listing tests returns list of tests.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.list_tests.return_value = { | |
| "total": 2, | |
| "tests": [ | |
| { | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "running", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "primary_metric": "satisfaction_rate", | |
| "winner": None | |
| }, | |
| { | |
| "test_id": "test-456", | |
| "name": "Test B", | |
| "status": "completed", | |
| "test_type": "agent_config", | |
| "agent_id": "agent-2", | |
| "primary_metric": "success_rate", | |
| "winner": "B" | |
| } | |
| ] | |
| } | |
| response = client.get("/api/ab-tests") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert "total" in data | |
| assert data["total"] == 2 | |
| def test_list_tests_filtered_by_agent(self, client): | |
| """Test listing tests filters by agent_id parameter.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.list_tests.return_value = { | |
| "total": 1, | |
| "tests": [{ | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "running", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "primary_metric": "satisfaction_rate", | |
| "winner": None | |
| }] | |
| } | |
| response = client.get("/api/ab-tests?agent_id=agent-1") | |
| assert response.status_code == 200 | |
| def test_list_tests_filtered_by_status(self, client): | |
| """Test listing tests filters by status parameter.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.list_tests.return_value = { | |
| "total": 1, | |
| "tests": [{ | |
| "test_id": "test-123", | |
| "name": "Test A", | |
| "status": "running", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "primary_metric": "satisfaction_rate", | |
| "winner": None | |
| }] | |
| } | |
| response = client.get("/api/ab-tests?status=running") | |
| assert response.status_code == 200 | |
| def test_list_tests_with_limit(self, client): | |
| """Test listing tests respects limit parameter.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.list_tests.return_value = { | |
| "total": 10, | |
| "tests": [] | |
| } | |
| response = client.get("/api/ab-tests?limit=10") | |
| assert response.status_code == 200 | |
| def test_list_tests_empty(self, client): | |
| """Test listing tests returns empty list when no tests.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.list_tests.return_value = { | |
| "total": 0, | |
| "tests": [] | |
| } | |
| response = client.get("/api/ab-tests") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["total"] == 0 | |
| def test_list_tests_limit_validation(self, client): | |
| """Test limit parameter enforces ge=1 and le=100 constraints.""" | |
| # Test limit < 1 | |
| response = client.get("/api/ab-tests?limit=0") | |
| assert response.status_code == 422 # Pydantic validation error | |
| # Test limit > 100 | |
| response = client.get("/api/ab-tests?limit=101") | |
| assert response.status_code == 422 | |
| # ============================================================================ | |
| # TestRequestModels - 4 tests | |
| # ============================================================================ | |
| class TestRequestModels: | |
| """Test suite for Pydantic request model validation.""" | |
| def client(self): | |
| """Create TestClient with A/B testing router.""" | |
| app = FastAPI() | |
| app.include_router(router) # Router already has prefix | |
| from fastapi.testclient import TestClient | |
| return TestClient(app) | |
| def test_create_test_request_required_fields(self, client): | |
| """Test CreateTestRequest validates required fields.""" | |
| response = client.post("/api/ab-tests/create", json={}) | |
| # Pydantic validation error | |
| assert response.status_code == 422 | |
| def test_create_test_request_optional_fields(self, client): | |
| """Test CreateTestRequest handles optional fields correctly.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "test_id": "test-123", | |
| "name": "Test", | |
| "status": "draft", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a": {"name": "Control", "config": {}}, | |
| "variant_b": {"name": "Treatment", "config": {}}, | |
| "primary_metric": "satisfaction_rate", | |
| "min_sample_size": 100, | |
| "traffic_percentage": 0.5 | |
| } | |
| # Without optional fields | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Test", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a_config": {}, | |
| "variant_b_config": {}, | |
| "primary_metric": "satisfaction_rate" | |
| }) | |
| assert response.status_code == 200 | |
| def test_assign_variant_request_required_fields(self, client): | |
| """Test AssignVariantRequest validates user_id required.""" | |
| response = client.post("/api/ab-tests/test-123/assign", json={}) | |
| assert response.status_code == 422 | |
| def test_record_metric_request_at_least_one(self, client): | |
| """Test RecordMetricRequest allows recording with just user_id.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| # Service allows recording with just user_id (success and metric_value are optional) | |
| mock_service.record_metric.return_value = { | |
| "test_id": "test-123", | |
| "user_id": "user-1", | |
| "variant": "A", | |
| "success": None, | |
| "metric_value": None, | |
| "recorded_at": "2026-03-12T11:00:00Z" | |
| } | |
| response = client.post("/api/ab-tests/test-123/record", json={ | |
| "user_id": "user-1" | |
| }) | |
| # Should succeed since both fields are optional | |
| assert response.status_code == 200 | |
| # ============================================================================ | |
| # TestErrorResponses - 4 tests | |
| # ============================================================================ | |
| class TestErrorResponses: | |
| """Test suite for error response format consistency.""" | |
| def client(self): | |
| """Create TestClient with A/B testing router.""" | |
| app = FastAPI() | |
| app.include_router(router) # Router already has prefix | |
| from fastapi.testclient import TestClient | |
| return TestClient(app) | |
| def test_error_response_format(self, client): | |
| """Test error responses have consistent format.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "error": "Invalid test configuration" | |
| } | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Invalid", | |
| "test_type": "invalid", | |
| "agent_id": "", | |
| "variant_a_config": {}, | |
| "variant_b_config": {}, | |
| "primary_metric": "" | |
| }) | |
| assert response.status_code == 400 | |
| data = response.json() | |
| # HTTPException wraps response in "detail" field | |
| assert "detail" in data | |
| assert data["detail"]["success"] is False | |
| def test_error_response_code_includes_ab_test_error(self, client): | |
| """Test error responses include AB_TEST_ERROR code.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.start_test.return_value = { | |
| "error": "Test not found" | |
| } | |
| response = client.post("/api/ab-tests/nonexistent/start") | |
| assert response.status_code == 400 | |
| def test_not_found_response_format(self, client): | |
| """Test 404 response format for not found.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.get_test_results.return_value = { | |
| "error": "Test 'nonexistent' not found" | |
| } | |
| response = client.get("/api/ab-tests/nonexistent/results") | |
| assert response.status_code == 404 | |
| def test_validation_error_response(self, client): | |
| """Test 400 response for validation errors.""" | |
| response = client.post("/api/ab-tests/create", json={}) | |
| assert response.status_code == 422 | |
| # ============================================================================ | |
| # TestTestTypes - 4 tests | |
| # ============================================================================ | |
| class TestTestTypes: | |
| """Test suite for different test types (agent_config, prompt, strategy, tool).""" | |
| def client(self): | |
| """Create TestClient with A/B testing router.""" | |
| app = FastAPI() | |
| app.include_router(router) # Router already has prefix | |
| from fastapi.testclient import TestClient | |
| return TestClient(app) | |
| def test_create_test_agent_config_type(self, client): | |
| """Test creating test with test_type='agent_config'.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "test_id": "test-config", | |
| "name": "Config Test", | |
| "status": "draft", | |
| "test_type": "agent_config", | |
| "agent_id": "agent-1", | |
| "variant_a": {"name": "Control", "config": {}}, | |
| "variant_b": {"name": "Treatment", "config": {}}, | |
| "primary_metric": "success_rate", | |
| "min_sample_size": 100, | |
| "traffic_percentage": 0.5 | |
| } | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Config Test", | |
| "test_type": "agent_config", | |
| "agent_id": "agent-1", | |
| "variant_a_config": {}, | |
| "variant_b_config": {}, | |
| "primary_metric": "success_rate" | |
| }) | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["test_type"] == "agent_config" | |
| def test_create_test_prompt_type(self, client): | |
| """Test creating test with test_type='prompt'.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "test_id": "test-prompt", | |
| "name": "Prompt Test", | |
| "status": "draft", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a": {"name": "Control", "config": {}}, | |
| "variant_b": {"name": "Treatment", "config": {}}, | |
| "primary_metric": "satisfaction_rate", | |
| "min_sample_size": 100, | |
| "traffic_percentage": 0.5 | |
| } | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Prompt Test", | |
| "test_type": "prompt", | |
| "agent_id": "agent-1", | |
| "variant_a_config": {}, | |
| "variant_b_config": {}, | |
| "primary_metric": "satisfaction_rate" | |
| }) | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["test_type"] == "prompt" | |
| def test_create_test_strategy_type(self, client): | |
| """Test creating test with test_type='strategy'.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "test_id": "test-strategy", | |
| "name": "Strategy Test", | |
| "status": "draft", | |
| "test_type": "strategy", | |
| "agent_id": "agent-1", | |
| "variant_a": {"name": "Control", "config": {}}, | |
| "variant_b": {"name": "Treatment", "config": {}}, | |
| "primary_metric": "success_rate", | |
| "min_sample_size": 100, | |
| "traffic_percentage": 0.5 | |
| } | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Strategy Test", | |
| "test_type": "strategy", | |
| "agent_id": "agent-1", | |
| "variant_a_config": {}, | |
| "variant_b_config": {}, | |
| "primary_metric": "success_rate" | |
| }) | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["test_type"] == "strategy" | |
| def test_create_test_tool_type(self, client): | |
| """Test creating test with test_type='tool'.""" | |
| with patch('api.ab_testing.ABTestingService') as MockService: | |
| mock_service = MagicMock() | |
| MockService.return_value = mock_service | |
| mock_service.create_test.return_value = { | |
| "test_id": "test-tool", | |
| "name": "Tool Test", | |
| "status": "draft", | |
| "test_type": "tool", | |
| "agent_id": "agent-1", | |
| "variant_a": {"name": "Control", "config": {}}, | |
| "variant_b": {"name": "Treatment", "config": {}}, | |
| "primary_metric": "success_rate", | |
| "min_sample_size": 100, | |
| "traffic_percentage": 0.5 | |
| } | |
| response = client.post("/api/ab-tests/create", json={ | |
| "name": "Tool Test", | |
| "test_type": "tool", | |
| "agent_id": "agent-1", | |
| "variant_a_config": {}, | |
| "variant_b_config": {}, | |
| "primary_metric": "success_rate" | |
| }) | |
| assert response.status_code == 200 | |
| assert response.json()["data"]["test_type"] == "tool" | |