diff --git a/packages/toolbox-adk/integration.cloudbuild.yaml b/packages/toolbox-adk/integration.cloudbuild.yaml index 8d8b5678f..353bad129 100644 --- a/packages/toolbox-adk/integration.cloudbuild.yaml +++ b/packages/toolbox-adk/integration.cloudbuild.yaml @@ -50,5 +50,5 @@ options: substitutions: _VERSION: '3.13' # Default values (can be overridden by triggers) - _TOOLBOX_VERSION: '1.5.0' + _TOOLBOX_VERSION: '1.6.0' _TOOLBOX_MANIFEST_VERSION: '34' diff --git a/packages/toolbox-adk/pyproject.toml b/packages/toolbox-adk/pyproject.toml index 3ad5c35e4..54b9a2002 100644 --- a/packages/toolbox-adk/pyproject.toml +++ b/packages/toolbox-adk/pyproject.toml @@ -40,7 +40,8 @@ test = [ "pytest==9.0.3", "pytest-asyncio==1.4.0", "pytest-cov==7.1.0", - "pytest-mock==3.15.1" + "pytest-mock==3.15.1", + "numpy<2", # Pinned to <2 to prevent mypy syntax errors on python 3.10 with numpy 2.x stubs, ] # Tells setuptools that packages are under the 'src' directory diff --git a/packages/toolbox-adk/tests/integration/conftest.py b/packages/toolbox-adk/tests/integration/conftest.py index 7a0346c3a..5c672547a 100644 --- a/packages/toolbox-adk/tests/integration/conftest.py +++ b/packages/toolbox-adk/tests/integration/conftest.py @@ -25,10 +25,13 @@ from typing import Generator import google -import pytest_asyncio +import pytest from google.auth import compute_engine from google.cloud import secretmanager, storage +TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" +TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" + #### Define Utility Functions def get_env_var(key: str) -> str: @@ -92,17 +95,17 @@ def get_auth_token(client_id: str) -> str: #### Define Fixtures -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def project_id() -> str: return get_env_var("GOOGLE_CLOUD_PROJECT") -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def toolbox_version() -> str: return get_env_var("TOOLBOX_VERSION") -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def tools_file_path(project_id: str) -> Generator[str]: """Provides a temporary file path containing the tools manifest.""" if os.environ.get("TEST_MOCK_GCP"): @@ -122,7 +125,7 @@ def tools_file_path(project_id: str) -> Generator[str]: os.remove(tools_file_path) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def auth_token1(project_id: str) -> str: if os.environ.get("TEST_MOCK_GCP"): return "mock-token-1" @@ -132,7 +135,7 @@ def auth_token1(project_id: str) -> str: return get_auth_token(client_id) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def auth_token2(project_id: str) -> str: if os.environ.get("TEST_MOCK_GCP"): return "mock-token-2" @@ -142,7 +145,7 @@ def auth_token2(project_id: str) -> str: return get_auth_token(client_id) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None]: """Starts the toolbox server as a subprocess.""" if os.environ.get("TEST_MOCK_GCP"): @@ -159,20 +162,30 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None # Make toolbox executable os.chmod("toolbox", 0o700) # Run toolbox binary - toolbox_server = subprocess.Popen( - ["./toolbox", "--tools-file", tools_file_path] + toolbox_server_1 = subprocess.Popen( + ["./toolbox", "--port", "5000", "--tools-file", tools_file_path] + ) + toolbox_server_2 = subprocess.Popen( + [ + "./toolbox", + "--port", + "5001", + "--tools-file", + tools_file_path, + "--enable-draft-specs", + ] ) # Wait for server to start # Retry logic with a timeout for _ in range(5): # retries time.sleep(2) - print("Checking if toolbox is successfully started...") - if toolbox_server.poll() is None: - print("Toolbox server started successfully.") + print("Checking if both toolbox servers are successfully started...") + if toolbox_server_1.poll() is None and toolbox_server_2.poll() is None: + print("Toolbox servers started successfully.") break else: - raise RuntimeError("Toolbox server failed to start after 5 retries.") + raise RuntimeError("Toolbox servers failed to start after 5 retries.") except subprocess.CalledProcessError as e: print(e.stderr.decode("utf-8")) print(e.stdout.decode("utf-8")) @@ -180,5 +193,31 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None yield # Clean up toolbox server - toolbox_server.terminate() - toolbox_server.wait(timeout=5) + toolbox_server_1.terminate() + toolbox_server_2.terminate() + toolbox_server_1.wait(timeout=5) + toolbox_server_2.wait(timeout=5) + + +@pytest.fixture( + params=[TOOLBOX_SERVER_URL_STABLE, TOOLBOX_SERVER_URL_DRAFT], scope="session" +) +def toolbox_server_url(request) -> str: + return request.param + + +@pytest.fixture() +def patch_toolbox_client_url(toolbox_server_url): + from toolbox_adk.toolset import ToolboxToolset + + original_init = ToolboxToolset.__init__ + + def new_init(self, server_url=TOOLBOX_SERVER_URL_STABLE, *args, **kwargs): + if server_url == TOOLBOX_SERVER_URL_STABLE: + server_url = toolbox_server_url + original_init(self, server_url, *args, **kwargs) + + from unittest.mock import patch + + with patch.object(ToolboxToolset, "__init__", new_init, create=True): + yield diff --git a/packages/toolbox-adk/tests/integration/test_integration.py b/packages/toolbox-adk/tests/integration/test_integration.py index 780b66d38..0f8a88466 100644 --- a/packages/toolbox-adk/tests/integration/test_integration.py +++ b/packages/toolbox-adk/tests/integration/test_integration.py @@ -33,6 +33,8 @@ from toolbox_adk import CredentialStrategy, ToolboxTool, ToolboxToolset +TOOLBOX_SERVER_URL = "http://localhost:5000" + # Ensure TOOLBOX_VERSION is set for the fixture if "TOOLBOX_VERSION" not in os.environ: os.environ["TOOLBOX_VERSION"] = "0.0.1" # Use a valid version or mock @@ -50,7 +52,7 @@ async def test_load_toolset_and_run(self): # Auth: TOOLBOX_IDENTITY for simplicity in this local test as we don't have ADK identity setup. toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, toolset_name="my-toolset", credentials=CredentialStrategy.toolbox_identity(), ) @@ -84,7 +86,7 @@ async def test_load_toolset_and_run(self): async def test_load_toolset_with_default_protocol(self): """Test initializing toolset with default protocol (MCP).""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, toolset_name="my-toolset", credentials=CredentialStrategy.toolbox_identity(), ) @@ -108,7 +110,7 @@ async def test_load_toolset_with_default_protocol(self): async def test_load_toolset_with_explicit_protocol(self): """Test initializing toolset with specific protocol (MCP_v20251125).""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, toolset_name="my-toolset", credentials=CredentialStrategy.toolbox_identity(), protocol=Protocol.MCP_v20251125, @@ -132,7 +134,7 @@ async def test_load_toolset_with_explicit_protocol(self): async def test_partial_loading_by_names(self): toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-n-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -152,7 +154,7 @@ async def test_partial_loading_by_names(self): async def test_bound_params_e2e(self): # Test binding param at toolset level toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-n-rows"], bound_params={"num_rows": "2"}, credentials=CredentialStrategy.toolbox_identity(), @@ -168,7 +170,7 @@ async def test_bound_params_e2e(self): async def test_3lo_flow_simulation(self): toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, # Load a specific tool that we know the arguments for tool_names=["get-n-rows"], credentials=CredentialStrategy.user_identity( @@ -265,7 +267,7 @@ async def test_3lo_flow_simulation(self): async def test_manual_token_integration(self): """Test the MANUAL_TOKEN strategy.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, toolset_name="my-toolset", credentials=CredentialStrategy.manual_token(token="fake-manual-token"), ) @@ -284,7 +286,7 @@ async def test_manual_credentials_integration(self): mock_creds.token = "fake-creds-token" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, toolset_name="my-toolset", credentials=CredentialStrategy.manual_credentials(credentials=mock_creds), ) @@ -297,7 +299,7 @@ async def test_manual_credentials_integration(self): async def test_api_key_integration(self): """Test the API_KEY strategy.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, toolset_name="my-toolset", credentials=CredentialStrategy.api_key(key="my-key", header_name="x-foo"), ) @@ -333,7 +335,7 @@ async def test_adk_integration_optional_params(self): # 3. Use in Toolset toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, toolset_name="my-toolset", credentials=strategy, ) @@ -357,7 +359,7 @@ async def test_header_collision(self): creds_token = "Bearer strategy-token" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, toolset_name="my-toolset", additional_headers={"Authorization": manual_override}, credentials=CredentialStrategy.manual_token(token="strategy-token"), @@ -391,7 +393,7 @@ async def test_load_toolset_specific( ): """Load a specific toolset""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, toolset_name=toolset_name, credentials=CredentialStrategy.toolbox_identity(), ) @@ -406,7 +408,7 @@ async def test_load_toolset_specific( async def test_load_toolset_default(self): """Load the default toolset, i.e. all tools.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, credentials=CredentialStrategy.toolbox_identity(), ) try: @@ -429,7 +431,7 @@ async def test_load_toolset_default(self): async def test_run_tool(self): """Invoke a tool.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-n-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -451,7 +453,7 @@ async def test_run_tool(self): async def test_run_tool_missing_params(self): """Invoke a tool with missing params.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-n-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -470,7 +472,7 @@ async def test_run_tool_missing_params(self): async def test_run_tool_wrong_param_type(self): """Invoke a tool with wrong param type.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-n-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -494,7 +496,7 @@ class TestBindParams: async def test_bind_params(self): """Bind a param to an existing tool.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-n-rows"], bound_params={"num_rows": "3"}, credentials=CredentialStrategy.toolbox_identity(), @@ -516,7 +518,7 @@ async def test_bind_params(self): async def test_bind_params_callable(self): """Bind a callable param to an existing tool.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-n-rows"], bound_params={"num_rows": lambda: "3"}, credentials=CredentialStrategy.toolbox_identity(), @@ -542,7 +544,7 @@ class TestAuth: async def test_run_tool_unauth_with_auth(self, auth_token2: str): """Tests running a tool that doesn't require auth, with auth provided.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-row-by-id"], auth_token_getters={"my-test-auth": lambda: auth_token2}, credentials=CredentialStrategy.toolbox_identity(), @@ -559,7 +561,7 @@ async def test_run_tool_unauth_with_auth(self, auth_token2: str): async def test_run_multiple_tools_unauth_with_auth(self, auth_token2: str): """Tests running multiple tools that don't require auth, verifying formatting of tool lists.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-row-by-id", "search-rows"], auth_token_getters={"my-test-auth": lambda: auth_token2}, credentials=CredentialStrategy.toolbox_identity(), @@ -576,7 +578,7 @@ async def test_run_multiple_tools_unauth_with_auth(self, auth_token2: str): async def test_run_multiple_tools_partial_auth_usage(self, auth_token2: str): """Tests that when some tokens are used and some aren't across diverse tools, only the truly unused tokens appear in the error.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=[ "get-row-by-id-auth", "search-rows", @@ -601,7 +603,7 @@ async def test_run_tool_no_auth(self): """Tests running a tool requiring auth without providing auth.""" # Note: We load it without auth getters. Invocation should fail. toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-row-by-id-auth"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -621,7 +623,7 @@ async def test_run_tool_no_auth(self): async def test_run_tool_wrong_auth(self, auth_token2: str): """Tests running a tool with incorrect auth.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-row-by-id-auth"], auth_token_getters={"my-test-auth": lambda: auth_token2}, credentials=CredentialStrategy.toolbox_identity(), @@ -642,7 +644,7 @@ async def test_run_tool_wrong_auth(self, auth_token2: str): async def test_run_tool_auth(self, auth_token1: str): """Tests running a tool with correct auth.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-row-by-id-auth"], auth_token_getters={"my-test-auth": lambda: auth_token1}, credentials=CredentialStrategy.toolbox_identity(), @@ -664,7 +666,7 @@ async def get_token_asynchronously(): return auth_token1 toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["get-row-by-id-auth"], auth_token_getters={"my-test-auth": get_token_asynchronously}, credentials=CredentialStrategy.toolbox_identity(), @@ -690,7 +692,7 @@ class TestOptionalParams: async def test_run_tool_with_optional_params_omitted(self): """Invoke a tool providing only the required parameter.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["search-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -709,7 +711,7 @@ async def test_run_tool_with_optional_params_omitted(self): async def test_run_tool_with_all_valid_params(self): """Invoke a tool providing all parameters.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["search-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -729,7 +731,7 @@ async def test_run_tool_with_all_valid_params(self): async def test_run_tool_with_missing_required_param(self): """Invoke a tool without its required parameter.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["search-rows"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -754,7 +756,7 @@ class TestMapParams: async def test_run_tool_with_map_params(self): """Invoke a tool with valid map parameters.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["process-data"], credentials=CredentialStrategy.toolbox_identity(), ) @@ -785,7 +787,7 @@ async def test_run_tool_with_map_params(self): async def test_run_tool_with_wrong_map_value_type(self): """Invoke a tool with a map parameter having the wrong value type.""" toolset = ToolboxToolset( - server_url="http://localhost:5000", + server_url=TOOLBOX_SERVER_URL, tool_names=["process-data"], credentials=CredentialStrategy.toolbox_identity(), ) diff --git a/packages/toolbox-core/integration.cloudbuild.yaml b/packages/toolbox-core/integration.cloudbuild.yaml index 17e388483..3dc6141ba 100644 --- a/packages/toolbox-core/integration.cloudbuild.yaml +++ b/packages/toolbox-core/integration.cloudbuild.yaml @@ -45,5 +45,5 @@ options: logging: CLOUD_LOGGING_ONLY substitutions: _VERSION: '3.13' - _TOOLBOX_VERSION: '1.5.0' + _TOOLBOX_VERSION: '1.6.0' _TOOLBOX_MANIFEST_VERSION: '34' diff --git a/packages/toolbox-core/pyproject.toml b/packages/toolbox-core/pyproject.toml index 6c2bf1a6d..6e44e0a11 100644 --- a/packages/toolbox-core/pyproject.toml +++ b/packages/toolbox-core/pyproject.toml @@ -60,7 +60,8 @@ test = [ "google-cloud-secret-manager==2.28.0", "google-cloud-storage==3.10.1", "aioresponses==0.7.8", - "toolbox-core[telemetry]" + "toolbox-core[telemetry]", + "numpy<2", # Pinned to <2 to prevent mypy syntax errors on python 3.10 with numpy 2.x stubs ] [build-system] requires = ["setuptools"] diff --git a/packages/toolbox-core/tests/conftest.py b/packages/toolbox-core/tests/conftest.py index 338d031c0..b78db9db6 100644 --- a/packages/toolbox-core/tests/conftest.py +++ b/packages/toolbox-core/tests/conftest.py @@ -25,10 +25,13 @@ from typing import Generator import google -import pytest_asyncio +import pytest from google.auth import compute_engine from google.cloud import secretmanager, storage +TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" +TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" + #### Define Utility Functions def get_env_var(key: str) -> str: @@ -92,17 +95,17 @@ def get_auth_token(client_id: str) -> str: #### Define Fixtures -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def project_id() -> str: return get_env_var("GOOGLE_CLOUD_PROJECT") -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def toolbox_version() -> str: return get_env_var("TOOLBOX_VERSION") -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def tools_file_path(project_id: str) -> Generator[str]: """Provides a temporary file path containing the tools manifest.""" tools_manifest = access_secret_version( @@ -115,7 +118,7 @@ def tools_file_path(project_id: str) -> Generator[str]: os.remove(tools_file_path) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def auth_token1(project_id: str) -> str: client_id = access_secret_version( project_id=project_id, secret_id="sdk_testing_client1" @@ -123,7 +126,7 @@ def auth_token1(project_id: str) -> str: return get_auth_token(client_id) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def auth_token2(project_id: str) -> str: client_id = access_secret_version( project_id=project_id, secret_id="sdk_testing_client2" @@ -131,7 +134,7 @@ def auth_token2(project_id: str) -> str: return get_auth_token(client_id) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None]: """Starts the toolbox server as a subprocess.""" print("Downloading toolbox binary from gcs bucket...") @@ -143,20 +146,30 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None # Make toolbox executable os.chmod("toolbox", 0o700) # Run toolbox binary - toolbox_server = subprocess.Popen( - ["./toolbox", "--tools-file", tools_file_path] + toolbox_server_1 = subprocess.Popen( + ["./toolbox", "--port", "5000", "--tools-file", tools_file_path] + ) + toolbox_server_2 = subprocess.Popen( + [ + "./toolbox", + "--port", + "5001", + "--tools-file", + tools_file_path, + "--enable-draft-specs", + ] ) # Wait for server to start # Retry logic with a timeout for _ in range(5): # retries time.sleep(2) - print("Checking if toolbox is successfully started...") - if toolbox_server.poll() is None: - print("Toolbox server started successfully.") + print("Checking if both toolbox servers are successfully started...") + if toolbox_server_1.poll() is None and toolbox_server_2.poll() is None: + print("Toolbox servers started successfully.") break else: - raise RuntimeError("Toolbox server failed to start after 5 retries.") + raise RuntimeError("Toolbox servers failed to start after 5 retries.") except subprocess.CalledProcessError as e: print(e.stderr.decode("utf-8")) print(e.stdout.decode("utf-8")) @@ -164,5 +177,39 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None yield # Clean up toolbox server - toolbox_server.terminate() - toolbox_server.wait(timeout=5) + toolbox_server_1.terminate() + toolbox_server_2.terminate() + toolbox_server_1.wait(timeout=5) + toolbox_server_2.wait(timeout=5) + + +@pytest.fixture( + params=[TOOLBOX_SERVER_URL_STABLE, TOOLBOX_SERVER_URL_DRAFT], scope="session" +) +def toolbox_server_url(request) -> str: + return request.param + + +@pytest.fixture() +def patch_toolbox_client_url(toolbox_server_url): + from toolbox_core.client import ToolboxClient + from toolbox_core.sync_client import ToolboxSyncClient + + original_init = ToolboxClient.__init__ + original_sync_init = ToolboxSyncClient.__init__ + + def new_init(self, url=TOOLBOX_SERVER_URL_STABLE, *args, **kwargs): + if url == TOOLBOX_SERVER_URL_STABLE: + url = toolbox_server_url + original_init(self, url, *args, **kwargs) + + def new_sync_init(self, url=TOOLBOX_SERVER_URL_STABLE, *args, **kwargs): + if url == TOOLBOX_SERVER_URL_STABLE: + url = toolbox_server_url + original_sync_init(self, url, *args, **kwargs) + + from unittest.mock import patch + + with patch.object(ToolboxClient, "__init__", new_init, create=True): + with patch.object(ToolboxSyncClient, "__init__", new_sync_init, create=True): + yield diff --git a/packages/toolbox-core/tests/test_e2e.py b/packages/toolbox-core/tests/test_e2e.py index 0b926e178..9f1f6b01f 100644 --- a/packages/toolbox-core/tests/test_e2e.py +++ b/packages/toolbox-core/tests/test_e2e.py @@ -23,6 +23,8 @@ from toolbox_core.protocol import Protocol from toolbox_core.tool import ToolboxTool +pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") + # --- Shared Fixtures Defined at Module Level --- @pytest_asyncio.fixture(scope="function") diff --git a/packages/toolbox-core/tests/test_e2e_mcp.py b/packages/toolbox-core/tests/test_e2e_mcp.py index c35695ffa..d9dc6a6cc 100644 --- a/packages/toolbox-core/tests/test_e2e_mcp.py +++ b/packages/toolbox-core/tests/test_e2e_mcp.py @@ -23,6 +23,8 @@ from toolbox_core.protocol import Protocol from toolbox_core.tool import ToolboxTool +pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") + # TODO: Include draft versions in E2E integration tests once the server # supports SEP-2575 (stateless MCP / Request-Metadata). @@ -100,20 +102,26 @@ async def test_run_tool_missing_params(self, get_n_rows_tool: ToolboxTool): with pytest.raises(TypeError, match="missing a required argument: 'num_rows'"): await get_n_rows_tool() - async def test_protocol_fallback_e2e(self): + async def test_protocol_fallback_e2e(self, toolbox_server_url): """Tests that a client using MCP_LATEST can fallback to an older protocol against a server that doesn't support the latest version.""" # The E2E server currently does not support DRAFT 2026, so this will trigger a fallback. async with ToolboxClient( - "http://localhost:5000", protocol=Protocol.MCP_LATEST + toolbox_server_url, protocol=Protocol.MCP_LATEST ) as client: tool = await client.load_tool("get-n-rows") response = await tool(num_rows="1") assert "row1" in response # Verify that fallback occurred by checking the transport's final protocol version - assert ( - client._ToolboxClient__transport._protocol_version - != Protocol.MCP_LATEST.value - ) + if "5000" in toolbox_server_url: + assert ( + client._ToolboxClient__transport._protocol_version + != Protocol.MCP_LATEST.value + ) + else: + assert ( + client._ToolboxClient__transport._protocol_version + == Protocol.MCP_LATEST.value + ) async def test_run_tool_wrong_param_type(self, get_n_rows_tool: ToolboxTool): """Invoke a tool with wrong param type.""" diff --git a/packages/toolbox-core/tests/test_sync_e2e.py b/packages/toolbox-core/tests/test_sync_e2e.py index a306cedc2..38b90b131 100644 --- a/packages/toolbox-core/tests/test_sync_e2e.py +++ b/packages/toolbox-core/tests/test_sync_e2e.py @@ -17,6 +17,8 @@ from toolbox_core.sync_client import ToolboxSyncClient from toolbox_core.sync_tool import ToolboxSyncTool +pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") + # --- Shared Fixtures Defined at Module Level --- @pytest.fixture(scope="module") diff --git a/packages/toolbox-core/tests/test_tool.py b/packages/toolbox-core/tests/test_tool.py index 1b23db3ae..9e5ad85b2 100644 --- a/packages/toolbox-core/tests/test_tool.py +++ b/packages/toolbox-core/tests/test_tool.py @@ -409,9 +409,14 @@ def test_tool_init_basic(http_session, sample_tool_params, sample_tool_descripti bound_params={}, client_headers={}, ) + relevant_warnings = [ + w + for w in record + if not issubclass(w.category, (ResourceWarning, DeprecationWarning)) + ] assert ( - len(record) == 0 - ), f"ToolboxTool instantiation unexpectedly warned: {[f'{w.category.__name__}: {w.message}' for w in record]}" + len(relevant_warnings) == 0 + ), f"ToolboxTool instantiation unexpectedly warned: {[f'{w.category.__name__}: {w.message}' for w in relevant_warnings]}" assert tool_instance.__name__ == TEST_TOOL_NAME assert inspect.iscoroutinefunction(tool_instance.__call__) diff --git a/packages/toolbox-langchain/integration.cloudbuild.yaml b/packages/toolbox-langchain/integration.cloudbuild.yaml index 39a34b5fe..db9cbc135 100644 --- a/packages/toolbox-langchain/integration.cloudbuild.yaml +++ b/packages/toolbox-langchain/integration.cloudbuild.yaml @@ -49,5 +49,5 @@ options: logging: CLOUD_LOGGING_ONLY substitutions: _VERSION: '3.13' - _TOOLBOX_VERSION: '1.5.0' + _TOOLBOX_VERSION: '1.6.0' _TOOLBOX_MANIFEST_VERSION: '34' diff --git a/packages/toolbox-langchain/pyproject.toml b/packages/toolbox-langchain/pyproject.toml index 247fdbfab..76e0845a1 100644 --- a/packages/toolbox-langchain/pyproject.toml +++ b/packages/toolbox-langchain/pyproject.toml @@ -53,6 +53,7 @@ test = [ "Pillow==12.2.0; python_version >= '3.10'", "google-cloud-secret-manager==2.28.0", "google-cloud-storage==3.10.1", + "numpy<2", # Pinned to <2 to prevent mypy syntax errors on python 3.10 with numpy 2.x stubs ] [build-system] diff --git a/packages/toolbox-langchain/tests/conftest.py b/packages/toolbox-langchain/tests/conftest.py index a1c87a7d8..05d9a175a 100644 --- a/packages/toolbox-langchain/tests/conftest.py +++ b/packages/toolbox-langchain/tests/conftest.py @@ -25,10 +25,13 @@ from typing import Generator import google -import pytest_asyncio +import pytest from google.auth import compute_engine from google.cloud import secretmanager, storage +TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" +TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" + #### Define Utility Functions def get_env_var(key: str) -> str: @@ -92,17 +95,17 @@ def get_auth_token(client_id: str) -> str: #### Define Fixtures -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def project_id() -> str: return get_env_var("GOOGLE_CLOUD_PROJECT") -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def toolbox_version() -> str: return get_env_var("TOOLBOX_VERSION") -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def tools_file_path(project_id: str) -> Generator[str]: """Provides a temporary file path containing the tools manifest.""" tools_manifest = access_secret_version( @@ -115,7 +118,7 @@ def tools_file_path(project_id: str) -> Generator[str]: os.remove(tools_file_path) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def auth_token1(project_id: str) -> str: client_id = access_secret_version( project_id=project_id, secret_id="sdk_testing_client1" @@ -123,7 +126,7 @@ def auth_token1(project_id: str) -> str: return get_auth_token(client_id) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def auth_token2(project_id: str) -> str: client_id = access_secret_version( project_id=project_id, secret_id="sdk_testing_client2" @@ -131,7 +134,7 @@ def auth_token2(project_id: str) -> str: return get_auth_token(client_id) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None]: """Starts the toolbox server as a subprocess.""" print("Downloading toolbox binary from gcs bucket...") @@ -143,20 +146,30 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None # Make toolbox executable os.chmod("toolbox", 0o700) # Run toolbox binary - toolbox_server = subprocess.Popen( - ["./toolbox", "--tools-file", tools_file_path] + toolbox_server_1 = subprocess.Popen( + ["./toolbox", "--port", "5000", "--tools-file", tools_file_path] + ) + toolbox_server_2 = subprocess.Popen( + [ + "./toolbox", + "--port", + "5001", + "--tools-file", + tools_file_path, + "--enable-draft-specs", + ] ) # Wait for server to start # Retry logic with a timeout for _ in range(5): # retries - time.sleep(2) - print("Checking if toolbox is successfully started...") - if toolbox_server.poll() is None: - print("Toolbox server started successfully.") + time.sleep(4) + print("Checking if both toolbox servers are successfully started...") + if toolbox_server_1.poll() is None and toolbox_server_2.poll() is None: + print("Toolbox servers started successfully.") break else: - raise RuntimeError("Toolbox server failed to start after 5 retries.") + raise RuntimeError("Toolbox servers failed to start after 5 retries.") except subprocess.CalledProcessError as e: print(e.stderr.decode("utf-8")) print(e.stdout.decode("utf-8")) @@ -164,5 +177,31 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None yield # Clean up toolbox server - toolbox_server.terminate() - toolbox_server.wait() + toolbox_server_1.terminate() + toolbox_server_2.terminate() + toolbox_server_1.wait() + toolbox_server_2.wait() + + +@pytest.fixture( + params=[TOOLBOX_SERVER_URL_STABLE, TOOLBOX_SERVER_URL_DRAFT], scope="session" +) +def toolbox_server_url(request) -> str: + return request.param + + +@pytest.fixture() +def patch_toolbox_client_url(toolbox_server_url): + from toolbox_core.client import ToolboxClient + + original_init = ToolboxClient.__init__ + + def new_init(self, url=TOOLBOX_SERVER_URL_STABLE, *args, **kwargs): + if url == TOOLBOX_SERVER_URL_STABLE: + url = toolbox_server_url + original_init(self, url, *args, **kwargs) + + from unittest.mock import patch + + with patch.object(ToolboxClient, "__init__", new_init, create=True): + yield diff --git a/packages/toolbox-langchain/tests/test_e2e.py b/packages/toolbox-langchain/tests/test_e2e.py index 93fbd01ea..8291cf4f4 100644 --- a/packages/toolbox-langchain/tests/test_e2e.py +++ b/packages/toolbox-langchain/tests/test_e2e.py @@ -40,6 +40,10 @@ from toolbox_langchain.client import ToolboxClient +pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") + +TOOLBOX_SERVER_URL = "http://localhost:5000" + @pytest.mark.asyncio @pytest.mark.usefixtures("toolbox_server") @@ -47,7 +51,7 @@ class TestE2EClientAsync: @pytest.fixture(scope="function") def toolbox(self): """Provides a ToolboxClient instance for each test.""" - toolbox = ToolboxClient("http://localhost:5000") + toolbox = ToolboxClient(TOOLBOX_SERVER_URL) return toolbox @pytest_asyncio.fixture(scope="function") @@ -194,7 +198,7 @@ class TestE2EClientSync: @pytest.fixture(scope="session") def toolbox(self): """Provides a ToolboxClient instance for each test.""" - toolbox = ToolboxClient("http://localhost:5000") + toolbox = ToolboxClient(TOOLBOX_SERVER_URL) return toolbox @pytest.fixture(scope="function") diff --git a/packages/toolbox-llamaindex/integration.cloudbuild.yaml b/packages/toolbox-llamaindex/integration.cloudbuild.yaml index 0d31e0d69..3a2e609aa 100644 --- a/packages/toolbox-llamaindex/integration.cloudbuild.yaml +++ b/packages/toolbox-llamaindex/integration.cloudbuild.yaml @@ -49,5 +49,5 @@ options: logging: CLOUD_LOGGING_ONLY substitutions: _VERSION: '3.13' - _TOOLBOX_VERSION: '1.5.0' + _TOOLBOX_VERSION: '1.6.0' _TOOLBOX_MANIFEST_VERSION: '34' diff --git a/packages/toolbox-llamaindex/pyproject.toml b/packages/toolbox-llamaindex/pyproject.toml index ec8107ead..fe801b2cc 100644 --- a/packages/toolbox-llamaindex/pyproject.toml +++ b/packages/toolbox-llamaindex/pyproject.toml @@ -53,6 +53,7 @@ test = [ "Pillow==12.2.0; python_version >= '3.10'", "google-cloud-secret-manager==2.28.0", "google-cloud-storage==3.10.1", + "numpy<2", # Pinned to <2 to prevent mypy syntax errors on python 3.10 with numpy 2.x stubs ] [build-system] diff --git a/packages/toolbox-llamaindex/tests/conftest.py b/packages/toolbox-llamaindex/tests/conftest.py index c8a8fa5f9..fa9784f1f 100644 --- a/packages/toolbox-llamaindex/tests/conftest.py +++ b/packages/toolbox-llamaindex/tests/conftest.py @@ -25,10 +25,13 @@ from typing import Generator import google -import pytest_asyncio +import pytest from google.auth import compute_engine from google.cloud import secretmanager, storage +TOOLBOX_SERVER_URL_STABLE = "http://localhost:5000" +TOOLBOX_SERVER_URL_DRAFT = "http://localhost:5001" + #### Define Utility Functions def get_env_var(key: str) -> str: @@ -92,17 +95,17 @@ def get_auth_token(client_id: str) -> str: #### Define Fixtures -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def project_id() -> str: return get_env_var("GOOGLE_CLOUD_PROJECT") -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def toolbox_version() -> str: return get_env_var("TOOLBOX_VERSION") -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def tools_file_path(project_id: str) -> Generator[str]: """Provides a temporary file path containing the tools manifest.""" tools_manifest = access_secret_version( @@ -115,7 +118,7 @@ def tools_file_path(project_id: str) -> Generator[str]: os.remove(tools_file_path) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def auth_token1(project_id: str) -> str: client_id = access_secret_version( project_id=project_id, secret_id="sdk_testing_client1" @@ -123,7 +126,7 @@ def auth_token1(project_id: str) -> str: return get_auth_token(client_id) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def auth_token2(project_id: str) -> str: client_id = access_secret_version( project_id=project_id, secret_id="sdk_testing_client2" @@ -131,7 +134,7 @@ def auth_token2(project_id: str) -> str: return get_auth_token(client_id) -@pytest_asyncio.fixture(scope="session") +@pytest.fixture(scope="session") def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None]: """Starts the toolbox server as a subprocess.""" print("Downloading toolbox binary from gcs bucket...") @@ -143,20 +146,30 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None # Make toolbox executable os.chmod("toolbox", 0o700) # Run toolbox binary - toolbox_server = subprocess.Popen( - ["./toolbox", "--tools-file", tools_file_path] + toolbox_server_1 = subprocess.Popen( + ["./toolbox", "--port", "5000", "--tools-file", tools_file_path] + ) + toolbox_server_2 = subprocess.Popen( + [ + "./toolbox", + "--port", + "5001", + "--tools-file", + tools_file_path, + "--enable-draft-specs", + ] ) # Wait for server to start # Retry logic with a timeout for _ in range(5): # retries time.sleep(4) - print("Checking if toolbox is successfully started...") - if toolbox_server.poll() is None: - print("Toolbox server started successfully.") + print("Checking if both toolbox servers are successfully started...") + if toolbox_server_1.poll() is None and toolbox_server_2.poll() is None: + print("Toolbox servers started successfully.") break else: - raise RuntimeError("Toolbox server failed to start after 5 retries.") + raise RuntimeError("Toolbox servers failed to start after 5 retries.") except subprocess.CalledProcessError as e: print(e.stderr.decode("utf-8")) print(e.stdout.decode("utf-8")) @@ -164,5 +177,31 @@ def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None yield # Clean up toolbox server - toolbox_server.terminate() - toolbox_server.wait() + toolbox_server_1.terminate() + toolbox_server_2.terminate() + toolbox_server_1.wait() + toolbox_server_2.wait() + + +@pytest.fixture( + params=[TOOLBOX_SERVER_URL_STABLE, TOOLBOX_SERVER_URL_DRAFT], scope="session" +) +def toolbox_server_url(request) -> str: + return request.param + + +@pytest.fixture() +def patch_toolbox_client_url(toolbox_server_url): + from toolbox_core.client import ToolboxClient + + original_init = ToolboxClient.__init__ + + def new_init(self, url=TOOLBOX_SERVER_URL_STABLE, *args, **kwargs): + if url == TOOLBOX_SERVER_URL_STABLE: + url = toolbox_server_url + original_init(self, url, *args, **kwargs) + + from unittest.mock import patch + + with patch.object(ToolboxClient, "__init__", new_init, create=True): + yield diff --git a/packages/toolbox-llamaindex/tests/test_e2e.py b/packages/toolbox-llamaindex/tests/test_e2e.py index e0cdba4cb..7664eeb32 100644 --- a/packages/toolbox-llamaindex/tests/test_e2e.py +++ b/packages/toolbox-llamaindex/tests/test_e2e.py @@ -40,6 +40,10 @@ from toolbox_llamaindex.client import ToolboxClient +pytestmark = pytest.mark.usefixtures("patch_toolbox_client_url") + +TOOLBOX_SERVER_URL = "http://localhost:5000" + @pytest.mark.asyncio @pytest.mark.usefixtures("toolbox_server") @@ -47,7 +51,7 @@ class TestE2EClientAsync: @pytest.fixture(scope="function") def toolbox(self): """Provides a ToolboxClient instance for each test.""" - toolbox = ToolboxClient("http://localhost:5000") + toolbox = ToolboxClient(TOOLBOX_SERVER_URL) return toolbox @pytest_asyncio.fixture(scope="function") @@ -194,7 +198,7 @@ class TestE2EClientSync: @pytest.fixture(scope="session") def toolbox(self): """Provides a ToolboxClient instance for each test.""" - toolbox = ToolboxClient("http://localhost:5000") + toolbox = ToolboxClient(TOOLBOX_SERVER_URL) return toolbox @pytest.fixture(scope="function")