diff --git a/galileo-a2a/pyproject.toml b/galileo-a2a/pyproject.toml index da6538e1..f3a9a925 100644 --- a/galileo-a2a/pyproject.toml +++ b/galileo-a2a/pyproject.toml @@ -69,7 +69,7 @@ python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] env = [ - "GALILEO_CONSOLE_URL=http://localtest:8088", + "GALILEO_CONSOLE_URL=http://fake.test:8088", "GALILEO_API_KEY=api-1234567890", "GALILEO_PROJECT=test-project", "GALILEO_LOG_STREAM=test-log-stream", diff --git a/galileo-a2a/tests/conftest.py b/galileo-a2a/tests/conftest.py index 217e599d..22e5d807 100644 --- a/galileo-a2a/tests/conftest.py +++ b/galileo-a2a/tests/conftest.py @@ -9,7 +9,7 @@ # 3. Security - prevents real API keys from leaking into test logs import os -os.environ["GALILEO_CONSOLE_URL"] = "http://localtest:8088" +os.environ["GALILEO_CONSOLE_URL"] = "http://fake.test:8088" os.environ["GALILEO_API_KEY"] = "api-1234567890" os.environ["GALILEO_PROJECT"] = "test-project" os.environ["GALILEO_LOG_STREAM"] = "test-log-stream" diff --git a/galileo-adk/pyproject.toml b/galileo-adk/pyproject.toml index f8be13b3..9c43cf5e 100644 --- a/galileo-adk/pyproject.toml +++ b/galileo-adk/pyproject.toml @@ -34,14 +34,16 @@ packages = ["src/galileo_adk"] [tool.pytest.ini_options] testpaths = ["tests"] -pythonpath = ["src"] +# ".." puts the monorepo root on sys.path so tests can import the shared +# `test_support` helper package (see test_support/config.py). +pythonpath = ["src", ".."] asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] env = [ - "GALILEO_CONSOLE_URL=http://localtest:8088", + "GALILEO_CONSOLE_URL=http://fake.test:8088", "GALILEO_API_KEY=api-1234567890", "GALILEO_PROJECT=test-project", "GALILEO_LOG_STREAM=test-log-stream", diff --git a/galileo-adk/tests/conftest.py b/galileo-adk/tests/conftest.py index 843976a5..4d46ef03 100644 --- a/galileo-adk/tests/conftest.py +++ b/galileo-adk/tests/conftest.py @@ -8,6 +8,7 @@ from galileo_core.constants.routes import Routes as CoreRoutes from galileo_core.schemas.core.user import User from galileo_core.schemas.core.user_role import UserRole +from test_support.config import fast_config_validation from galileo.config import GalileoPythonConfig from galileo.utils.singleton import GalileoLoggerSingleton @@ -168,7 +169,11 @@ def set_validated_config( # Reset any cached loggers from previous tests GalileoLoggerSingleton().reset_all() - config = GalileoPythonConfig.get(console_url="http://localtest:8088", api_key="api-1234567890") + # Bypass the slow async validation round-trips for the build only; the + # endpoints are already mocked above, so this only removes event-loop cost + # (notably the ~11x slower Windows IOCP poll on Python 3.11+). + with fast_config_validation(): + config = GalileoPythonConfig.get(console_url="http://fake.test:8088", api_key="api-1234567890") yield # Clean up after test GalileoLoggerSingleton().reset_all() diff --git a/pyproject.toml b/pyproject.toml index 779fceff..634c7341 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ pythonpath = ["./src/"] # Note: Some env vars are also set in conftest.py for pytest-xdist compatibility # on Python 3.14+. This section remains for documentation and older Python support. env = [ - "GALILEO_CONSOLE_URL=http://localtest:8088", + "GALILEO_CONSOLE_URL=http://fake.test:8088", "GALILEO_API_KEY=api-1234567890", "GALILEO_PROJECT=test-project", "GALILEO_LOG_STREAM=test-log-stream", diff --git a/test_support/__init__.py b/test_support/__init__.py new file mode 100644 index 00000000..cced59ba --- /dev/null +++ b/test_support/__init__.py @@ -0,0 +1,9 @@ +"""Repo-level test-support helpers shared across the main package and the +sibling integration packages (galileo-adk, galileo-a2a). + +This package is intentionally NOT shipped (it lives outside ``src/``) and is +not a pytest test package — it only holds importable helpers. It sits at the +repo root, rather than under ``tests/``, so the sibling packages (which have +their own top-level ``tests`` package) can import it without a package-name +collision. +""" diff --git a/test_support/config.py b/test_support/config.py new file mode 100644 index 00000000..d10bcdf3 --- /dev/null +++ b/test_support/config.py @@ -0,0 +1,59 @@ +"""Fast GalileoPythonConfig validation for tests. + +Building ``GalileoPythonConfig`` runs 3 async validation requests +(healthcheck/login/current_user) through galileo_core's ``async_run`` / +``EventLoopThreadPool``, whose Windows IOCP poll is ~11x slower on Python +3.11+. In tests these endpoints are already mocked, so they add no coverage — +only event-loop cost. ``fast_config_validation`` replaces them with canned, +await-free results so the per-test config build is trivial. + +Scope the context manager to the per-test config build only; test bodies still +exercise the real validation/connect code with their own mocks. + +Shared by the autouse ``set_validated_config`` fixtures in the main package and +in galileo-adk. +""" + +from collections.abc import Generator +from contextlib import contextmanager +from typing import Any +from unittest.mock import patch +from uuid import uuid4 + +from galileo_core.helpers.api_client import ApiClient +from galileo_core.schemas.core.user import User +from galileo_core.schemas.core.user_role import UserRole + + +def fast_validation_payload(endpoint: Any) -> dict: + """Canned response for the 3 config-validation endpoints.""" + ep = str(endpoint) + if "login" in ep or "token" in ep: + return {"access_token": "secret_jwt_token"} + if "current_user" in ep: + return User.model_validate({"id": uuid4(), "email": "user@example.com", "role": UserRole.user}).model_dump( + mode="json" + ) + return {"status": "ok"} + + +@contextmanager +def fast_config_validation() -> Generator[None, None, None]: + """Stub the async config-validation round-trips with canned, await-free + results so the per-test ``GalileoPythonConfig`` build is cheap. + + Scoped to the config build only; test bodies still exercise the real + validation/connect code. + """ + + async def _stub_make_request(request_method: Any, base_url: str, endpoint: Any, **kwargs: Any) -> dict: + return fast_validation_payload(endpoint) + + def _stub_request(self: Any, request_method: Any, path: Any = None, **kwargs: Any) -> dict: + return fast_validation_payload(path) + + with ( + patch.object(ApiClient, "make_request", staticmethod(_stub_make_request)), + patch.object(ApiClient, "request", _stub_request), + ): + yield diff --git a/tests/conftest.py b/tests/conftest.py index 82bac162..8d853f96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,7 +22,7 @@ ) from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails -_os.environ["SPLUNK_AO_CONSOLE_URL"] = "http://localtest:8088" +_os.environ["SPLUNK_AO_CONSOLE_URL"] = "http://fake.test:8088" _os.environ["SPLUNK_AO_API_KEY"] = "api-1234567890" _os.environ["SPLUNK_AO_PROJECT"] = "test-project" _os.environ["SPLUNK_AO_LOG_STREAM"] = "test-log-stream" @@ -50,6 +50,7 @@ from httpx import Request # noqa: E402 from httpx import Response as HttpxResponse # noqa: E402 +from test_support.config import fast_config_validation # noqa: E402 from galileo.collaborator import CollaboratorRole # noqa: E402 from galileo.config import GalileoPythonConfig # noqa: E402 @@ -125,8 +126,10 @@ def set_validated_config( if GalileoPythonConfig._instance is not None: GalileoPythonConfig._instance.reset() # Initialize config with EXPLICIT values to avoid env var timing issues with pytest-xdist - # This ensures correct config even if env vars weren't set before module imports - config = GalileoPythonConfig.get(console_url="http://localtest:8088", api_key="api-1234567890") + # This ensures correct config even if env vars weren't set before module imports. + # Bypass the slow async validation round-trips for the build only. + with fast_config_validation(): + config = GalileoPythonConfig.get(console_url="http://fake.test:8088", api_key="api-1234567890") yield config.reset() diff --git a/tests/test_experiments.py b/tests/test_experiments.py index 289d0b3f..c9e37ab6 100644 --- a/tests/test_experiments.py +++ b/tests/test_experiments.py @@ -674,7 +674,7 @@ def test_run_experiment_without_metrics( prompt_settings=ANY, ) - @pytest.mark.parametrize("console_url", ["http://localtest:8088", "http://localtest:8088/"]) + @pytest.mark.parametrize("console_url", ["http://fake.test:8088", "http://fake.test:8088/"]) @travel(datetime(2012, 1, 1), tick=False) @patch.object(galileo.datasets.Datasets, "get") @patch.object(galileo.jobs.Jobs, "create") diff --git a/tests/test_prompts_global.py b/tests/test_prompts_global.py index af0089f5..3ca5716f 100644 --- a/tests/test_prompts_global.py +++ b/tests/test_prompts_global.py @@ -62,12 +62,12 @@ class TestGlobalPromptTemplates: def test_create_global_prompt(self, respx_mock: MockRouter, prompt_template_response): """Test creating a global prompt template.""" # Mock the query API (for uniqueness check) - query_route = respx_mock.post("http://localtest:8088/templates/query").mock( + query_route = respx_mock.post("http://fake.test:8088/templates/query").mock( return_value=httpx.Response(200, json={"templates": []}) ) # Mock the create API - create_route = respx_mock.post("http://localtest:8088/templates").mock( + create_route = respx_mock.post("http://fake.test:8088/templates").mock( return_value=httpx.Response(200, json=prompt_template_response) ) @@ -80,7 +80,7 @@ def test_create_global_prompt(self, respx_mock: MockRouter, prompt_template_resp def test_get_global_prompt_by_id(self, respx_mock: MockRouter, prompt_template_response): """Test retrieving a global prompt template by ID.""" - get_route = respx_mock.get(f"http://localtest:8088/templates/{prompt_template_response['id']}").mock( + get_route = respx_mock.get(f"http://fake.test:8088/templates/{prompt_template_response['id']}").mock( return_value=httpx.Response(200, json=prompt_template_response) ) @@ -92,7 +92,7 @@ def test_get_global_prompt_by_id(self, respx_mock: MockRouter, prompt_template_r def test_get_global_prompt_by_name(self, respx_mock: MockRouter, prompt_template_response): """Test retrieving a global prompt template by name.""" - query_route = respx_mock.post("http://localtest:8088/templates/query").mock( + query_route = respx_mock.post("http://fake.test:8088/templates/query").mock( return_value=httpx.Response( 200, json={"templates": [prompt_template_response], "next_starting_token": None} ) @@ -106,7 +106,7 @@ def test_get_global_prompt_by_name(self, respx_mock: MockRouter, prompt_template def test_list_global_prompts(self, respx_mock: MockRouter, prompt_template_response): """Test listing global prompt templates.""" - query_route = respx_mock.post("http://localtest:8088/templates/query").mock( + query_route = respx_mock.post("http://fake.test:8088/templates/query").mock( return_value=httpx.Response( 200, json={"templates": [prompt_template_response], "next_starting_token": None} ) @@ -120,7 +120,7 @@ def test_list_global_prompts(self, respx_mock: MockRouter, prompt_template_respo def test_delete_global_prompt_by_id(self, respx_mock: MockRouter): """Test deleting a global prompt template by ID.""" - delete_route = respx_mock.delete("http://localtest:8088/templates/template-id-123").mock( + delete_route = respx_mock.delete("http://fake.test:8088/templates/template-id-123").mock( return_value=httpx.Response(200, json={"message": "Template deleted successfully"}) ) @@ -131,14 +131,14 @@ def test_delete_global_prompt_by_id(self, respx_mock: MockRouter): def test_delete_global_prompt_by_name(self, respx_mock: MockRouter, prompt_template_response): """Test deleting a global prompt template by name.""" # Mock query to find template by name - query_route = respx_mock.post("http://localtest:8088/templates/query").mock( + query_route = respx_mock.post("http://fake.test:8088/templates/query").mock( return_value=httpx.Response( 200, json={"templates": [prompt_template_response], "next_starting_token": None} ) ) # Mock delete - delete_route = respx_mock.delete(f"http://localtest:8088/templates/{prompt_template_response['id']}").mock( + delete_route = respx_mock.delete(f"http://fake.test:8088/templates/{prompt_template_response['id']}").mock( return_value=httpx.Response(200, json={"message": "Template deleted successfully"}) ) @@ -151,13 +151,13 @@ def test_create_prompt_with_unique_name(self, respx_mock: MockRouter, prompt_tem """Test that duplicate names get auto-incremented.""" # Mock query to find existing template existing_template = {**prompt_template_response, "name": "test-template"} - query_route = respx_mock.post("http://localtest:8088/templates/query").mock( + query_route = respx_mock.post("http://fake.test:8088/templates/query").mock( return_value=httpx.Response(200, json={"templates": [existing_template], "next_starting_token": None}) ) # Mock create with new unique name new_template = {**prompt_template_response, "name": "test-template (1)"} - create_route = respx_mock.post("http://localtest:8088/templates").mock( + create_route = respx_mock.post("http://fake.test:8088/templates").mock( return_value=httpx.Response(200, json=new_template) )