"""Tests for SGLang backend — detection, runtime creation, serve --backend flag, FastAPI app.""" from unittest.mock import MagicMock from unittest.mock import patch as mock_patch import pytest def _has_fastapi(): try: import fastapi # noqa: F401 return True except ImportError: return False # ─── SGLang Detection Tests ────────────────────────────────────────────── class TestSGLangDetection: """Test SGLang availability detection.""" def test_check_sglang_available_when_installed(self): """check_sglang_available should return True when sglang is importable.""" mock_sglang = MagicMock() with mock_patch.dict("sys.modules", {"sglang": mock_sglang}): from soup_cli.utils.sglang import check_sglang_available assert check_sglang_available() is True def test_check_sglang_available_when_not_installed(self): """check_sglang_available should return False when sglang import fails.""" from soup_cli.utils.sglang import check_sglang_available # Just verify the function exists and is callable assert callable(check_sglang_available) def test_get_sglang_version_when_installed(self): """get_sglang_version should return version string.""" mock_sglang = MagicMock() mock_sglang.__version__ = "0.3.0" with mock_patch.dict("sys.modules", {"sglang": mock_sglang}): from soup_cli.utils.sglang import get_sglang_version version = get_sglang_version() assert version == "0.3.0" def test_get_sglang_version_when_not_installed(self): """get_sglang_version should return 'not installed' when unavailable.""" from soup_cli.utils.sglang import get_sglang_version # If sglang is not actually installed, should return "not installed" try: import sglang # noqa: F401 except ImportError: assert get_sglang_version() == "not installed" # ─── SGLang Runtime Creation Tests ─────────────────────────────────────── class TestSGLangRuntimeCreation: """Test SGLang runtime creation.""" def test_create_sglang_runtime_full_model(self): """create_sglang_runtime should create runtime for a full model.""" mock_runtime = MagicMock() mock_sgl = MagicMock() mock_sgl.Runtime.return_value = mock_runtime with mock_patch.dict("sys.modules", {"sglang": mock_sgl}): from soup_cli.utils.sglang import create_sglang_runtime runtime, model_name = create_sglang_runtime( model_path="/path/to/model", ) mock_sgl.Runtime.assert_called_once() call_kwargs = mock_sgl.Runtime.call_args[1] assert call_kwargs["model_path"] == "/path/to/model" assert call_kwargs["trust_remote_code"] is True assert model_name == "/path/to/model" def test_create_sglang_runtime_with_adapter(self): """create_sglang_runtime should pass lora_paths for adapter.""" mock_runtime = MagicMock() mock_sgl = MagicMock() mock_sgl.Runtime.return_value = mock_runtime with mock_patch.dict("sys.modules", {"sglang": mock_sgl}): from soup_cli.utils.sglang import create_sglang_runtime runtime, model_name = create_sglang_runtime( model_path="/path/to/adapter", base_model="meta-llama/Llama-3.1-8B", is_adapter=True, ) call_kwargs = mock_sgl.Runtime.call_args[1] assert call_kwargs["model_path"] == "meta-llama/Llama-3.1-8B" assert call_kwargs["lora_paths"] == ["/path/to/adapter"] assert model_name == "meta-llama/Llama-3.1-8B" def test_create_sglang_runtime_tensor_parallel(self): """create_sglang_runtime should pass tp_size.""" mock_sgl = MagicMock() mock_sgl.Runtime.return_value = MagicMock() with mock_patch.dict("sys.modules", {"sglang": mock_sgl}): from soup_cli.utils.sglang import create_sglang_runtime create_sglang_runtime( model_path="/model", tensor_parallel_size=4, ) call_kwargs = mock_sgl.Runtime.call_args[1] assert call_kwargs["tp_size"] == 4 # ─── SGLang FastAPI App Tests ──────────────────────────────────────────── @pytest.mark.skipif( not _has_fastapi(), reason="fastapi not installed", ) class TestSGLangApp: """Test SGLang FastAPI app creation (requires fastapi).""" def test_create_sglang_app_returns_fastapi(self): """create_sglang_app should return a FastAPI application.""" mock_runtime = MagicMock() from soup_cli.utils.sglang import create_sglang_app app = create_sglang_app( runtime=mock_runtime, runtime_model_name="test-model", model_name="test", ) assert app is not None def test_sglang_app_has_health_endpoint(self): """SGLang app should have /health endpoint.""" mock_runtime = MagicMock() from soup_cli.utils.sglang import create_sglang_app app = create_sglang_app( runtime=mock_runtime, runtime_model_name="test-model", model_name="test", ) routes = [route.path for route in app.routes] assert "/health" in routes def test_sglang_app_has_models_endpoint(self): """SGLang app should have /v1/models endpoint.""" mock_runtime = MagicMock() from soup_cli.utils.sglang import create_sglang_app app = create_sglang_app( runtime=mock_runtime, runtime_model_name="test-model", model_name="test", ) routes = [route.path for route in app.routes] assert "/v1/models" in routes def test_sglang_app_has_chat_completions_endpoint(self): """SGLang app should have /v1/chat/completions endpoint.""" mock_runtime = MagicMock() from soup_cli.utils.sglang import create_sglang_app app = create_sglang_app( runtime=mock_runtime, runtime_model_name="test-model", model_name="test", ) routes = [route.path for route in app.routes] assert "/v1/chat/completions" in routes # ─── Serve Command SGLang Tests ────────────────────────────────────────── class TestServeSGLangCommand: """Test serve command with --backend sglang.""" def test_serve_sglang_backend_in_help(self): """soup serve --help should mention sglang backend.""" from typer.testing import CliRunner from soup_cli.cli import app runner = CliRunner() result = runner.invoke(app, ["serve", "--help"]) assert "sglang" in result.output def test_serve_sglang_not_installed_shows_error(self, tmp_path): """serve --backend sglang should show install hint when sglang missing.""" from typer.testing import CliRunner from soup_cli.cli import app # Create a dummy model path model_dir = tmp_path / "model" model_dir.mkdir() runner = CliRunner() with mock_patch( "soup_cli.utils.sglang.check_sglang_available", return_value=False ): result = runner.invoke(app, [ "serve", "--model", str(model_dir), "--backend", "sglang", ]) assert result.exit_code != 0 @pytest.mark.skipif(not _has_fastapi(), reason="fastapi not installed") def test_serve_unknown_backend_shows_error(self, tmp_path): """serve --backend invalid should show error listing valid backends.""" from typer.testing import CliRunner from soup_cli.cli import app model_dir = tmp_path / "model" model_dir.mkdir() runner = CliRunner() result = runner.invoke(app, [ "serve", "--model", str(model_dir), "--backend", "invalid_backend", ]) assert result.exit_code != 0 assert "sglang" in result.output # Should list valid backends # ─── Doctor SGLang Detection Tests ─────────────────────────────────────── class TestDoctorSGLang: """Test that soup doctor checks for SGLang.""" def test_doctor_includes_sglang(self): """DEPS list should include sglang.""" from soup_cli.commands.doctor import DEPS pkg_names = [dep[1] for dep in DEPS] assert "sglang" in pkg_names def test_doctor_includes_librosa(self): """DEPS list should include librosa.""" from soup_cli.commands.doctor import DEPS pkg_names = [dep[1] for dep in DEPS] assert "librosa" in pkg_names # ─── SGLang SSRF Validation Tests ──────────────────────────────────────── class TestSGLangSSRF: """Test SSRF protection in SGLang runtime creation.""" def test_create_runtime_blocks_http_model_path(self): """HTTP URLs for model_path should be rejected.""" mock_sgl = MagicMock() with mock_patch.dict("sys.modules", {"sglang": mock_sgl}): from soup_cli.utils.sglang import create_sglang_runtime with pytest.raises(ValueError, match="not a URL"): create_sglang_runtime( model_path="http://evil.com/model", ) def test_create_runtime_blocks_http_base_model(self): """HTTP URLs for base_model should be rejected.""" mock_sgl = MagicMock() with mock_patch.dict("sys.modules", {"sglang": mock_sgl}): from soup_cli.utils.sglang import create_sglang_runtime with pytest.raises(ValueError, match="not a URL"): create_sglang_runtime( model_path="/path/to/adapter", base_model="https://evil.com/model", is_adapter=True, ) def test_create_runtime_allows_hf_model_id(self): """HuggingFace model IDs should be allowed.""" mock_sgl = MagicMock() mock_sgl.Runtime.return_value = MagicMock() with mock_patch.dict("sys.modules", {"sglang": mock_sgl}): from soup_cli.utils.sglang import create_sglang_runtime runtime, name = create_sglang_runtime( model_path="meta-llama/Llama-3.1-8B", ) assert name == "meta-llama/Llama-3.1-8B"