diff --git a/examples/local_browser_playwright_example.py b/examples/local_browser_playwright_example.py index f5107d57..d3f208df 100644 --- a/examples/local_browser_playwright_example.py +++ b/examples/local_browser_playwright_example.py @@ -104,7 +104,6 @@ def main() -> None: browserbase_api_key=bb_api_key, browserbase_project_id=bb_project_id, model_api_key=model_api_key, - local_openai_api_key=model_api_key, local_ready_timeout_s=30.0, ) as client: print("⏳ Starting Stagehand session (local server + local browser)...") diff --git a/examples/local_example.py b/examples/local_example.py index e9bddd07..440c69ac 100644 --- a/examples/local_example.py +++ b/examples/local_example.py @@ -5,7 +5,7 @@ Required environment variables: - BROWSERBASE_API_KEY (can be any value in local mode) - BROWSERBASE_PROJECT_ID (can be any value in local mode) -- MODEL_API_KEY (used for client configuration even in local mode) +- MODEL_API_KEY (read by this example and passed explicitly to the client) Install the published wheel before running this script: @@ -45,13 +45,14 @@ def _stream_to_result(stream, label: str) -> object | None: def main() -> None: load_example_env() + # The example reads MODEL_API_KEY itself so the SDK configuration stays explicit. model_key = os.environ.get("MODEL_API_KEY") if not model_key: sys.exit("Set MODEL_API_KEY to run the local server.") client = Stagehand( server="local", - local_openai_api_key=model_key, + model_api_key=model_key, local_ready_timeout_s=30.0, ) diff --git a/examples/local_server_multiregion_browser_example.py b/examples/local_server_multiregion_browser_example.py index ca6ac89f..2bb1ed61 100644 --- a/examples/local_server_multiregion_browser_example.py +++ b/examples/local_server_multiregion_browser_example.py @@ -75,7 +75,6 @@ def main() -> None: browserbase_api_key=bb_api_key, browserbase_project_id=bb_project_id, model_api_key=model_api_key, - local_openai_api_key=model_api_key, local_ready_timeout_s=30.0, ) as client: print("⏳ Starting Stagehand session (local server + Browserbase browser)...") diff --git a/src/stagehand/_client.py b/src/stagehand/_client.py index 3997b5e1..7204dcf1 100644 --- a/src/stagehand/_client.py +++ b/src/stagehand/_client.py @@ -24,7 +24,7 @@ from ._models import FinalRequestOptions from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream -from ._exceptions import APIStatusError, StagehandError +from ._exceptions import APIStatusError from ._base_client import ( DEFAULT_MAX_RETRIES, SyncAPIClient, @@ -52,7 +52,7 @@ class Stagehand(SyncAPIClient): # client options browserbase_api_key: str | None browserbase_project_id: str | None - model_api_key: str + model_api_key: str | None def __init__( self, @@ -67,7 +67,6 @@ def __init__( local_headless: bool = True, local_chrome_path: str | None = None, local_ready_timeout_s: float = 10.0, - local_openai_api_key: str | None = None, local_shutdown_on_close: bool = True, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, @@ -93,7 +92,10 @@ def __init__( This automatically infers the following arguments from their corresponding environment variables if they are not provided: - `browserbase_api_key` from `BROWSERBASE_API_KEY` - `browserbase_project_id` from `BROWSERBASE_PROJECT_ID` - - `model_api_key` from `MODEL_API_KEY` + + `model_api_key` is intentionally not inferred from any AI provider environment variable. + Pass it explicitly when you want the SDK to send `x-model-api-key` on remote requests or + to forward `MODEL_API_KEY` to the local SEA child process. """ self._server_mode: Literal["remote", "local"] = server self._local_stagehand_binary_path = _local_stagehand_binary_path @@ -102,7 +104,6 @@ def __init__( self._local_headless = local_headless self._local_chrome_path = local_chrome_path self._local_ready_timeout_s = local_ready_timeout_s - self._local_openai_api_key = local_openai_api_key self._local_shutdown_on_close = local_shutdown_on_close if browserbase_api_key is None: @@ -113,12 +114,6 @@ def __init__( self.browserbase_api_key = browserbase_api_key self.browserbase_project_id = browserbase_project_id - if model_api_key is None: - model_api_key = os.environ.get("MODEL_API_KEY") - if model_api_key is None: - raise StagehandError( - "The model_api_key client option must be set either by passing model_api_key to the client or by setting the MODEL_API_KEY environment variable" - ) self.model_api_key = model_api_key self._sea_server: SeaServerManager | None = None @@ -127,14 +122,13 @@ def __init__( if base_url is None: base_url = "http://127.0.0.1" - openai_api_key = local_openai_api_key or os.environ.get("OPENAI_API_KEY") or model_api_key self._sea_server = SeaServerManager( config=SeaServerConfig( host=local_host, port=local_port, headless=local_headless, ready_timeout_s=local_ready_timeout_s, - openai_api_key=openai_api_key, + model_api_key=model_api_key, chrome_path=local_chrome_path, shutdown_on_close=local_shutdown_on_close, ), @@ -210,7 +204,7 @@ def _bb_project_id_auth(self) -> dict[str, str]: @property def _llm_model_api_key_auth(self) -> dict[str, str]: model_api_key = self.model_api_key - return {"x-model-api-key": model_api_key} + return {"x-model-api-key": model_api_key} if model_api_key else {} @property @override @@ -236,7 +230,6 @@ def copy( local_headless: bool | None = None, local_chrome_path: str | None = None, local_ready_timeout_s: float | None = None, - local_openai_api_key: str | None = None, local_shutdown_on_close: bool | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, @@ -283,9 +276,6 @@ def copy( local_ready_timeout_s=local_ready_timeout_s if local_ready_timeout_s is not None else self._local_ready_timeout_s, - local_openai_api_key=local_openai_api_key - if local_openai_api_key is not None - else self._local_openai_api_key, local_shutdown_on_close=local_shutdown_on_close if local_shutdown_on_close is not None else self._local_shutdown_on_close, @@ -340,7 +330,7 @@ class AsyncStagehand(AsyncAPIClient): # client options browserbase_api_key: str | None browserbase_project_id: str | None - model_api_key: str + model_api_key: str | None def __init__( self, @@ -355,7 +345,6 @@ def __init__( local_headless: bool = True, local_chrome_path: str | None = None, local_ready_timeout_s: float = 10.0, - local_openai_api_key: str | None = None, local_shutdown_on_close: bool = True, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, @@ -381,7 +370,10 @@ def __init__( This automatically infers the following arguments from their corresponding environment variables if they are not provided: - `browserbase_api_key` from `BROWSERBASE_API_KEY` - `browserbase_project_id` from `BROWSERBASE_PROJECT_ID` - - `model_api_key` from `MODEL_API_KEY` + + `model_api_key` is intentionally not inferred from any AI provider environment variable. + Pass it explicitly when you want the SDK to send `x-model-api-key` on remote requests or + to forward `MODEL_API_KEY` to the local SEA child process. """ self._server_mode: Literal["remote", "local"] = server self._local_stagehand_binary_path = _local_stagehand_binary_path @@ -390,7 +382,6 @@ def __init__( self._local_headless = local_headless self._local_chrome_path = local_chrome_path self._local_ready_timeout_s = local_ready_timeout_s - self._local_openai_api_key = local_openai_api_key self._local_shutdown_on_close = local_shutdown_on_close if browserbase_api_key is None: @@ -401,12 +392,6 @@ def __init__( self.browserbase_api_key = browserbase_api_key self.browserbase_project_id = browserbase_project_id - if model_api_key is None: - model_api_key = os.environ.get("MODEL_API_KEY") - if model_api_key is None: - raise StagehandError( - "The model_api_key client option must be set either by passing model_api_key to the client or by setting the MODEL_API_KEY environment variable" - ) self.model_api_key = model_api_key self._sea_server: SeaServerManager | None = None @@ -414,14 +399,13 @@ def __init__( if base_url is None: base_url = "http://127.0.0.1" - openai_api_key = local_openai_api_key or os.environ.get("OPENAI_API_KEY") or model_api_key self._sea_server = SeaServerManager( config=SeaServerConfig( host=local_host, port=local_port, headless=local_headless, ready_timeout_s=local_ready_timeout_s, - openai_api_key=openai_api_key, + model_api_key=model_api_key, chrome_path=local_chrome_path, shutdown_on_close=local_shutdown_on_close, ), @@ -497,7 +481,7 @@ def _bb_project_id_auth(self) -> dict[str, str]: @property def _llm_model_api_key_auth(self) -> dict[str, str]: model_api_key = self.model_api_key - return {"x-model-api-key": model_api_key} + return {"x-model-api-key": model_api_key} if model_api_key else {} @property @override @@ -523,7 +507,6 @@ def copy( local_headless: bool | None = None, local_chrome_path: str | None = None, local_ready_timeout_s: float | None = None, - local_openai_api_key: str | None = None, local_shutdown_on_close: bool | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, @@ -570,9 +553,6 @@ def copy( local_ready_timeout_s=local_ready_timeout_s if local_ready_timeout_s is not None else self._local_ready_timeout_s, - local_openai_api_key=local_openai_api_key - if local_openai_api_key is not None - else self._local_openai_api_key, local_shutdown_on_close=local_shutdown_on_close if local_shutdown_on_close is not None else self._local_shutdown_on_close, diff --git a/src/stagehand/lib/sea_server.py b/src/stagehand/lib/sea_server.py index baed6da0..ef34aa62 100644 --- a/src/stagehand/lib/sea_server.py +++ b/src/stagehand/lib/sea_server.py @@ -24,7 +24,7 @@ class SeaServerConfig: port: int headless: bool ready_timeout_s: float - openai_api_key: str | None + model_api_key: str | None chrome_path: str | None shutdown_on_close: bool @@ -118,6 +118,26 @@ def __init__( def base_url(self) -> str | None: return self._base_url + def _build_process_env(self, *, port: int) -> dict[str, str]: + proc_env = dict(os.environ) + # Force production mode so inherited NODE_ENV=development never reaches the + # SEA child process. Development mode breaks under SEA because pino-pretty + # is an optional dependency that is not present in the packaged binary. + proc_env["NODE_ENV"] = "production" + # Server package expects BB_ENV to be set (see packages/server/src/lib/env.ts) + proc_env.setdefault("BB_ENV", "local") + proc_env["HOST"] = self._config.host + proc_env["PORT"] = str(port) + proc_env["HEADLESS"] = "true" if self._config.headless else "false" + # Always set MODEL_API_KEY in the child env so the SDK constructor value wins + # over any inherited parent MODEL_API_KEY. An empty string preserves the + # "explicitly unset" case instead of silently reusing the parent's value. + proc_env["MODEL_API_KEY"] = self._config.model_api_key or "" + if self._config.chrome_path: + proc_env["CHROME_PATH"] = self._config.chrome_path + proc_env["LIGHTHOUSE_CHROMIUM_PATH"] = self._config.chrome_path + return proc_env + def ensure_running_sync(self) -> str: with self._lock: if self._proc is not None and self._proc.poll() is None and self._base_url is not None: @@ -169,20 +189,7 @@ def _start_sync(self) -> tuple[str, subprocess.Popen[bytes]]: port = _pick_free_port(self._config.host) if self._config.port == 0 else self._config.port base_url = _build_base_url(host=self._config.host, port=port) - - proc_env = dict(os.environ) - # Defaults that make the server boot under SEA (avoid pino-pretty transport) - proc_env.setdefault("NODE_ENV", "production") - # Server package expects BB_ENV to be set (see packages/server/src/lib/env.ts) - proc_env.setdefault("BB_ENV", "local") - proc_env["HOST"] = self._config.host - proc_env["PORT"] = str(port) - proc_env["HEADLESS"] = "true" if self._config.headless else "false" - if self._config.openai_api_key: - proc_env["OPENAI_API_KEY"] = self._config.openai_api_key - if self._config.chrome_path: - proc_env["CHROME_PATH"] = self._config.chrome_path - proc_env["LIGHTHOUSE_CHROMIUM_PATH"] = self._config.chrome_path + proc_env = self._build_process_env(port=port) preexec_fn = None creationflags = 0 @@ -221,18 +228,7 @@ async def _start_async(self) -> tuple[str, subprocess.Popen[bytes]]: port = _pick_free_port(self._config.host) if self._config.port == 0 else self._config.port base_url = _build_base_url(host=self._config.host, port=port) - - proc_env = dict(os.environ) - proc_env.setdefault("NODE_ENV", "production") - proc_env.setdefault("BB_ENV", "local") - proc_env["HOST"] = self._config.host - proc_env["PORT"] = str(port) - proc_env["HEADLESS"] = "true" if self._config.headless else "false" - if self._config.openai_api_key: - proc_env["OPENAI_API_KEY"] = self._config.openai_api_key - if self._config.chrome_path: - proc_env["CHROME_PATH"] = self._config.chrome_path - proc_env["LIGHTHOUSE_CHROMIUM_PATH"] = self._config.chrome_path + proc_env = self._build_process_env(port=port) preexec_fn = None creationflags = 0 diff --git a/tests/test_client.py b/tests/test_client.py index 95758e1e..42cb3374 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -24,7 +24,7 @@ from stagehand._utils import asyncify from stagehand._models import BaseModel, FinalRequestOptions from stagehand._streaming import Stream, AsyncStream -from stagehand._exceptions import APIStatusError, StagehandError, APITimeoutError, APIResponseValidationError +from stagehand._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError from stagehand._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, @@ -464,22 +464,21 @@ def test_validate_headers(self) -> None: request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-model-api-key") == model_api_key - with pytest.raises(StagehandError): - with update_env( - **{ - "BROWSERBASE_API_KEY": Omit(), - "BROWSERBASE_PROJECT_ID": Omit(), - "MODEL_API_KEY": Omit(), - } - ): - client2 = Stagehand( - base_url=base_url, - browserbase_api_key=None, - browserbase_project_id=None, - model_api_key=None, - _strict_response_validation=True, - ) - client2.sessions.start(model_name="openai/gpt-5-nano") + with update_env( + BROWSERBASE_API_KEY=Omit(), + BROWSERBASE_PROJECT_ID=Omit(), + ): + client2 = Stagehand( + base_url=base_url, + browserbase_api_key=None, + browserbase_project_id=None, + model_api_key=None, + _strict_response_validation=True, + ) + request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-bb-api-key") is None + assert request.headers.get("x-bb-project-id") is None + assert request.headers.get("x-model-api-key") is None def test_default_query_option(self) -> None: client = Stagehand( @@ -1512,22 +1511,21 @@ def test_validate_headers(self) -> None: request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-model-api-key") == model_api_key - with pytest.raises(StagehandError): - with update_env( - **{ - "BROWSERBASE_API_KEY": Omit(), - "BROWSERBASE_PROJECT_ID": Omit(), - "MODEL_API_KEY": Omit(), - } - ): - client2 = AsyncStagehand( - base_url=base_url, - browserbase_api_key=None, - browserbase_project_id=None, - model_api_key=None, - _strict_response_validation=True, - ) - _ = client2 + with update_env( + BROWSERBASE_API_KEY=Omit(), + BROWSERBASE_PROJECT_ID=Omit(), + ): + client2 = AsyncStagehand( + base_url=base_url, + browserbase_api_key=None, + browserbase_project_id=None, + model_api_key=None, + _strict_response_validation=True, + ) + request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-bb-api-key") is None + assert request.headers.get("x-bb-project-id") is None + assert request.headers.get("x-model-api-key") is None async def test_default_query_option(self) -> None: client = AsyncStagehand( diff --git a/tests/test_local_server.py b/tests/test_local_server.py index ebf44686..490ccbda 100644 --- a/tests/test_local_server.py +++ b/tests/test_local_server.py @@ -1,12 +1,15 @@ from __future__ import annotations +import os import json +from pathlib import Path import httpx import pytest from respx import MockRouter from stagehand import Stagehand, AsyncStagehand +from stagehand.lib import sea_server from stagehand._exceptions import StagehandError @@ -31,10 +34,80 @@ async def aclose(self) -> None: self.closed += 1 +class _DummyProcess: + pid = 12345 + + def __init__(self) -> None: + self._returncode: int | None = None + + def poll(self) -> int | None: + return self._returncode + + def wait(self, _timeout: float | None = None) -> int: + self._returncode = 0 + return 0 + + def terminate(self) -> None: + self._returncode = 0 + + def kill(self) -> None: + self._returncode = 0 + + def _set_required_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("BROWSERBASE_API_KEY", "bb_key") monkeypatch.setenv("BROWSERBASE_PROJECT_ID", "bb_project") - monkeypatch.setenv("MODEL_API_KEY", "model_key") + + +def _set_browserbase_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("BROWSERBASE_API_KEY", "bb_key") + monkeypatch.setenv("BROWSERBASE_PROJECT_ID", "bb_project") + + +def _install_fake_sea_runtime( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + captured_env: dict[str, str], + *, + port: int, +) -> None: + binary_path = tmp_path / "stagehand-test-binary" + binary_path.write_text("binary") + + def _fake_popen( + args: list[str], + env: dict[str, str], + stdout: object, + stderr: object, + preexec_fn: object, + creationflags: int, + ) -> _DummyProcess: + del args, stdout, stderr, preexec_fn, creationflags + captured_env.update(env) + return _DummyProcess() + + def _fake_pick_free_port(_host: str) -> int: + return port + + def _fake_terminate_process(proc: _DummyProcess) -> None: + proc._returncode = 0 + + def _fake_wait_ready_sync(*, base_url: str, timeout_s: float) -> None: + del base_url, timeout_s + + def _fake_resolve_binary_path( + *, + _local_stagehand_binary_path: str | os.PathLike[str] | None = None, + version: str | None = None, + ) -> Path: + del _local_stagehand_binary_path, version + return binary_path + + monkeypatch.setattr(sea_server, "_pick_free_port", _fake_pick_free_port) + monkeypatch.setattr(sea_server, "_terminate_process", _fake_terminate_process) + monkeypatch.setattr(sea_server, "_wait_ready_sync", _fake_wait_ready_sync) + monkeypatch.setattr(sea_server, "resolve_binary_path", _fake_resolve_binary_path) + monkeypatch.setattr(sea_server.subprocess, "Popen", _fake_popen) @pytest.mark.respx(base_url="http://127.0.0.1:43123") @@ -57,7 +130,11 @@ def test_sync_local_mode_starts_before_first_request(respx_mock: MockRouter, mon ) ) - client = Stagehand(server="local", _local_stagehand_binary_path="/does/not/matter/in/test") + client = Stagehand( + server="local", + model_api_key="model_key", + _local_stagehand_binary_path="/does/not/matter/in/test", + ) # Swap in a dummy server so we don't spawn a real binary in unit tests. client._sea_server = dummy # type: ignore[attr-defined] @@ -94,7 +171,11 @@ def _capture_start_request(request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/sessions/start").mock(side_effect=_capture_start_request) - client = Stagehand(server="local", _local_stagehand_binary_path="/does/not/matter/in/test") + client = Stagehand( + server="local", + model_api_key="model_key", + _local_stagehand_binary_path="/does/not/matter/in/test", + ) client._sea_server = dummy # type: ignore[attr-defined] resp = client.sessions.start(model_name="openai/gpt-5-nano") @@ -131,7 +212,11 @@ async def test_async_local_mode_starts_before_first_request( ) ) - async with AsyncStagehand(server="local", _local_stagehand_binary_path="/does/not/matter/in/test") as client: + async with AsyncStagehand( + server="local", + model_api_key="model_key", + _local_stagehand_binary_path="/does/not/matter/in/test", + ) as client: client._sea_server = dummy # type: ignore[attr-defined] resp = await client.sessions.start(model_name="openai/gpt-5-nano") assert resp.success is True @@ -146,7 +231,11 @@ def test_local_server_requires_browserbase_keys_for_browserbase_sessions( _set_required_env(monkeypatch) monkeypatch.delenv("BROWSERBASE_API_KEY", raising=False) monkeypatch.delenv("BROWSERBASE_PROJECT_ID", raising=False) - client = Stagehand(server="local", _local_stagehand_binary_path="/does/not/matter/in/test") + client = Stagehand( + server="local", + model_api_key="model_key", + _local_stagehand_binary_path="/does/not/matter/in/test", + ) client._sea_server = _DummySeaServer("http://127.0.0.1:43125") # type: ignore[attr-defined] with pytest.raises(StagehandError): client.sessions.start(model_name="openai/gpt-5-nano") @@ -158,7 +247,11 @@ def test_local_server_allows_local_browser_without_browserbase_keys( _set_required_env(monkeypatch) monkeypatch.delenv("BROWSERBASE_API_KEY", raising=False) monkeypatch.delenv("BROWSERBASE_PROJECT_ID", raising=False) - client = Stagehand(server="local", _local_stagehand_binary_path="/does/not/matter/in/test") + client = Stagehand( + server="local", + model_api_key="model_key", + _local_stagehand_binary_path="/does/not/matter/in/test", + ) client._sea_server = _DummySeaServer("http://127.0.0.1:43126") # type: ignore[attr-defined] def _post(*_args: object, **_kwargs: object) -> object: @@ -172,3 +265,70 @@ def _post(*_args: object, **_kwargs: object) -> object: model_name="openai/gpt-5-nano", browser={"type": "local"}, ) + + +@pytest.mark.parametrize( + ("explicit_model_api_key", "expected_model_api_key"), + [ + (None, ""), + ("good1", "good1"), + ], +) +def test_local_mode_masks_inherited_model_api_key_envs_and_prefers_explicit_param( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + explicit_model_api_key: str | None, + expected_model_api_key: str, +) -> None: + _set_browserbase_env(monkeypatch) + # Simulate a parent process with conflicting inherited env. The child SEA process + # should keep unrelated env intact while MODEL_API_KEY follows constructor intent. + monkeypatch.setenv("MODEL_API_KEY", "bad1") + monkeypatch.setenv("OPENAI_API_KEY", "bad2") + + captured_env: dict[str, str] = {} + _install_fake_sea_runtime(monkeypatch, tmp_path, captured_env, port=43129) + + if explicit_model_api_key is None: + client = Stagehand( + server="local", + _local_stagehand_binary_path="/does/not/matter/in/test", + ) + else: + client = Stagehand( + server="local", + model_api_key=explicit_model_api_key, + _local_stagehand_binary_path="/does/not/matter/in/test", + ) + assert client._sea_server is not None + + client._sea_server.ensure_running_sync() + + assert captured_env["MODEL_API_KEY"] == expected_model_api_key + assert captured_env["OPENAI_API_KEY"] == "bad2" + client.close() + + +def test_local_mode_forwards_flow_log_and_config_dir_env_to_sea_binary( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + _set_required_env(monkeypatch) + monkeypatch.setenv("BROWSERBASE_FLOW_LOGS", "1") + monkeypatch.setenv("BROWSERBASE_CONFIG_DIR", "./tmp") + + captured_env: dict[str, str] = {} + _install_fake_sea_runtime(monkeypatch, tmp_path, captured_env, port=43131) + + client = Stagehand( + server="local", + model_api_key="model_key", + _local_stagehand_binary_path="/does/not/matter/in/test", + ) + assert client._sea_server is not None + + client._sea_server.ensure_running_sync() + + assert captured_env["BROWSERBASE_FLOW_LOGS"] == "1" + assert captured_env["BROWSERBASE_CONFIG_DIR"] == "./tmp" + client.close() diff --git a/tests/test_sea_binary.py b/tests/test_sea_binary.py index 1ad9c8eb..300543a8 100644 --- a/tests/test_sea_binary.py +++ b/tests/test_sea_binary.py @@ -32,7 +32,11 @@ def test_resolve_binary_path_defaults_cache_version_to_package_version( captured: dict[str, object] = {} + # This test is exercising the packaged-resource path, so clear the env override + # that would otherwise bypass _resource_binary_path() entirely. + monkeypatch.delenv("STAGEHAND_SEA_BINARY", raising=False) monkeypatch.delenv("STAGEHAND_VERSION", raising=False) + def _fake_resource_binary_path(_filename: str) -> Path: return resource_path