mirror of https://github.com/razor-ai/soup.git
317 lines
10 KiB
Python
317 lines
10 KiB
Python
"""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"]
|