"""Tests for Web UI Visual Config Builder — schema, recipes, form-to-YAML.""" import pytest def _auth_headers(): """Return auth headers with the current UI token.""" from soup_cli.ui.app import get_auth_token return {"Authorization": f"Bearer {get_auth_token()}"} class TestConfigSchemaEndpoint: """Test GET /api/config/schema endpoint.""" def test_schema_endpoint_exists(self): """Route /api/config/schema should be registered.""" try: import fastapi # noqa: F401 except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app app = create_app() routes = [route.path for route in app.routes] assert "/api/config/schema" in routes def test_schema_returns_json(self): """Schema endpoint should return valid JSON.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.get("/api/config/schema") assert response.status_code == 200 data = response.json() assert isinstance(data, dict) def test_schema_includes_base_field(self): """Schema should include 'base' as a required string field.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.get("/api/config/schema") data = response.json() assert "base" in data assert data["base"]["type"] == "string" assert data["base"]["required"] is True def test_schema_includes_task_enum(self): """Schema should include 'task' with enum options.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.get("/api/config/schema") data = response.json() assert "task" in data assert "options" in data["task"] assert "sft" in data["task"]["options"] assert "dpo" in data["task"]["options"] def test_schema_includes_training_fields(self): """Schema should include nested training fields.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.get("/api/config/schema") data = response.json() assert "training" in data training = data["training"] assert "epochs" in training assert "lr" in training def test_schema_includes_constraints(self): """Schema should include ge/le constraints for bounded fields.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.get("/api/config/schema") data = response.json() # Check that training.epochs has constraints or type info training = data.get("training", {}) if "epochs" in training: assert training["epochs"]["type"] in ("integer", "number") class TestRecipesEndpoint: """Test GET /api/recipes endpoint.""" def test_recipes_endpoint_exists(self): """Route /api/recipes should be registered.""" try: import fastapi # noqa: F401 except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app app = create_app() routes = [route.path for route in app.routes] assert "/api/recipes" in routes def test_recipes_returns_catalog(self): """Recipes endpoint should return all recipes.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.get("/api/recipes") assert response.status_code == 200 data = response.json() assert "recipes" in data assert len(data["recipes"]) >= 20 # We have 29 recipes def test_recipes_have_required_fields(self): """Each recipe should have name, model, task fields.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.get("/api/recipes") recipes = response.json()["recipes"] for recipe in recipes: assert "name" in recipe assert "model" in recipe assert "task" in recipe assert "yaml" in recipe class TestFormToYamlEndpoint: """Test POST /api/config/from-form endpoint.""" def test_form_to_yaml_endpoint_exists(self): """Route /api/config/from-form should be registered.""" try: import fastapi # noqa: F401 except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app app = create_app() routes = [route.path for route in app.routes] assert "/api/config/from-form" in routes def test_form_to_yaml_produces_valid_config(self): """Form-to-YAML should produce a valid config string.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.post( "/api/config/from-form", json={ "base": "meta-llama/Llama-3.1-8B-Instruct", "task": "sft", "data": {"train": "./data.jsonl", "format": "alpaca"}, "training": {"epochs": 3, "lr": 2e-4}, }, headers=_auth_headers(), ) assert response.status_code == 200 data = response.json() assert "yaml" in data assert "base:" in data["yaml"] assert "meta-llama" in data["yaml"] def test_form_to_yaml_validates(self): """Form-to-YAML should validate via load_config_from_string.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.post( "/api/config/from-form", json={ "base": "test-model", "data": {"train": "./data.jsonl"}, }, headers=_auth_headers(), ) assert response.status_code == 200 data = response.json() assert "yaml" in data def test_form_to_yaml_invalid_returns_error(self): """Invalid form values should return error.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) # Missing base — should fail validation response = client.post( "/api/config/from-form", json={"task": "sft"}, headers=_auth_headers(), ) assert response.status_code in (200, 400) if response.status_code == 200: data = response.json() assert "error" in data def test_form_to_yaml_requires_auth(self): """Form-to-YAML (POST) should require auth.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.post( "/api/config/from-form", json={"base": "test"}, ) assert response.status_code == 401 def test_form_to_yaml_special_chars_in_keys(self): """Form values with special YAML characters should be handled safely.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.post( "/api/config/from-form", json={ "base": "test: model\noutput: /etc", "data": {"train": "./data.jsonl"}, }, headers=_auth_headers(), ) assert response.status_code == 200 data = response.json() # The result should either be valid YAML or an error — never injection if "yaml" in data: # The colon/newline in base value should be quoted by yaml.dump assert "base:" in data["yaml"] elif "error" in data: # Validation error is also acceptable pass def test_form_to_yaml_with_lora(self): """Form-to-YAML should handle nested lora config.""" try: from fastapi.testclient import TestClient except ImportError: pytest.skip("FastAPI not installed") from soup_cli.ui.app import create_app client = TestClient(create_app()) response = client.post( "/api/config/from-form", json={ "base": "test-model", "data": {"train": "./data.jsonl"}, "training": { "epochs": 3, "lora": {"r": 16, "alpha": 32}, }, }, headers=_auth_headers(), ) assert response.status_code == 200 data = response.json() assert "yaml" in data assert "lora:" in data["yaml"] or "r:" in data["yaml"]