From 133f5b6cccb6914d4fa90c622c6a7681b0902554 Mon Sep 17 00:00:00 2001 From: Mouse Date: Tue, 14 Apr 2026 10:18:29 -0700 Subject: [PATCH 1/5] feat: add you.com search provider --- README.md | 22 ++++++ docs/tools.md | 22 +++++- src/leeway/tools/web_search_tool.py | 90 ++++++++++++++++------- tests/test_tools/test_web_search_tool.py | 93 ++++++++++++++++++++++++ 4 files changed, 199 insertions(+), 28 deletions(-) create mode 100644 tests/test_tools/test_web_search_tool.py diff --git a/README.md b/README.md index 2ce53b8..3535359 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,28 @@ See **[docs/workflows.md](docs/workflows.md)** for the full pattern catalog and --- +### Web Search Provider Setup + +`web_search` supports two providers via environment variables: + +```bash +# Default provider (backward compatible) +export WEB_SEARCH_PROVIDER=brave +export BRAVE_SEARCH_API_KEY=your_brave_key + +# Optional provider: you.com Search API +export WEB_SEARCH_PROVIDER=you +export YOU_SEARCH_API_KEY=your_you_api_key +``` + +Usage in prompts/workflows remains unchanged: + +```text +Use web_search with query: "latest model context protocol updates" +``` + +--- + ## Learn More | Topic | Docs | diff --git a/docs/tools.md b/docs/tools.md index 6fcf8d3..cfd86c0 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -3,7 +3,7 @@ | Category | Tools | Description | |----------|-------|-------------| | **File I/O** | `bash`, `read_file`, `write_file`, `edit_file`, `glob`, `grep` | Core file operations with permission checks | -| **Web** | `web_fetch`, `web_search` | HTTP content retrieval and Brave search | +| **Web** | `web_fetch`, `web_search` | HTTP content retrieval and configurable web search (Brave or you.com) | | **Interaction** | `ask_user_question`, `skill` | User input and on-demand knowledge loading | | **Tasks** | `task_create`, `task_list`, `task_get`, `task_stop` | Background task lifecycle management | | **Scheduling** | `cron_create`, `cron_list`, `cron_delete`, `cron_toggle` | Cron job management | @@ -13,6 +13,26 @@ Every tool has **Pydantic input validation**, **self-describing JSON Schema**, **permission integration**, and **hook support**. +## Web Search Provider Setup + +`web_search` supports two providers via environment variables: + +```bash +# Default provider (backward compatible) +export WEB_SEARCH_PROVIDER=brave +export BRAVE_SEARCH_API_KEY=your_brave_key + +# Optional provider: you.com Search API +export WEB_SEARCH_PROVIDER=you +export YOU_SEARCH_API_KEY=your_you_api_key +``` + +Usage in prompts/workflows remains unchanged: + +```text +Use web_search with query: "latest model context protocol updates" +``` + ## Custom Tool ```python diff --git a/src/leeway/tools/web_search_tool.py b/src/leeway/tools/web_search_tool.py index d87d93f..eb0b6a3 100644 --- a/src/leeway/tools/web_search_tool.py +++ b/src/leeway/tools/web_search_tool.py @@ -1,7 +1,9 @@ -"""Web search tool — search the web via Brave Search API.""" +"""Web search tool — search the web via Brave or you.com Search API.""" from __future__ import annotations +import os + from pydantic import BaseModel, Field from leeway.tools.base import BaseTool, ToolExecutionContext, ToolResult @@ -17,8 +19,9 @@ class WebSearchInput(BaseModel): class WebSearchTool(BaseTool): """Search the web and return results. - Requires a Brave Search API key configured via - ``web_search_api_key`` in settings or ``BRAVE_SEARCH_API_KEY`` env var. + Provider selection: + - ``WEB_SEARCH_PROVIDER=brave`` (default) requires ``BRAVE_SEARCH_API_KEY`` + - ``WEB_SEARCH_PROVIDER=you`` requires ``YOU_SEARCH_API_KEY`` """ name = "web_search" @@ -39,42 +42,75 @@ async def execute(self, arguments: BaseModel, context: ToolExecutionContext) -> is_error=True, ) - import os - - api_key = os.environ.get("BRAVE_SEARCH_API_KEY", "") - if not api_key: + provider = os.environ.get("WEB_SEARCH_PROVIDER", "brave").strip().lower() + if provider not in {"brave", "you"}: return ToolResult( - output=( - "No search API key found. Set BRAVE_SEARCH_API_KEY environment " - "variable or configure web_search_api_key in settings." - ), + output="Unsupported WEB_SEARCH_PROVIDER. Use 'brave' or 'you'.", is_error=True, ) - try: - async with httpx.AsyncClient(timeout=15.0) as client: - resp = await client.get( - "https://api.search.brave.com/res/v1/web/search", - params={"q": args.query, "count": args.num_results}, - headers={ - "Accept": "application/json", - "X-Subscription-Token": api_key, - }, + if provider == "you": + api_key = os.environ.get("YOU_SEARCH_API_KEY", "") + if not api_key: + return ToolResult( + output=( + "No API key found for you.com search. Set YOU_SEARCH_API_KEY " + "or switch WEB_SEARCH_PROVIDER=brave." + ), + is_error=True, ) - resp.raise_for_status() - except Exception as exc: - return ToolResult(output=f"Search failed: {exc}", is_error=True) - data = resp.json() - results = data.get("web", {}).get("results", []) + try: + async with httpx.AsyncClient(timeout=15.0) as client: + resp = await client.get( + "https://api.ydc-index.io/v1/search", + params={"query": args.query, "num_web_results": args.num_results}, + headers={"X-API-Key": api_key}, + ) + resp.raise_for_status() + except Exception as exc: + return ToolResult(output=f"you.com search failed: {exc}", is_error=True) + + data = resp.json() + results = data.get("hits", []) + else: + api_key = os.environ.get("BRAVE_SEARCH_API_KEY", "") + if not api_key: + return ToolResult( + output=( + "No API key found for Brave search. Set BRAVE_SEARCH_API_KEY " + "or switch WEB_SEARCH_PROVIDER=you with YOU_SEARCH_API_KEY." + ), + is_error=True, + ) + + try: + async with httpx.AsyncClient(timeout=15.0) as client: + resp = await client.get( + "https://api.search.brave.com/res/v1/web/search", + params={"q": args.query, "count": args.num_results}, + headers={ + "Accept": "application/json", + "X-Subscription-Token": api_key, + }, + ) + resp.raise_for_status() + except Exception as exc: + return ToolResult(output=f"Brave search failed: {exc}", is_error=True) + + data = resp.json() + results = data.get("web", {}).get("results", []) + if not results: return ToolResult(output="No results found.") lines: list[str] = [] for i, r in enumerate(results[: args.num_results], 1): - title = r.get("title", "") + title = r.get("title") or r.get("name", "") url = r.get("url", "") - desc = r.get("description", "") + snippets = r.get("snippets") or [] + first_snippet = snippets[0] if snippets else "" + desc = first_snippet or r.get("description") or r.get("snippet") or "" lines.append(f"{i}. {title}\n {url}\n {desc}") return ToolResult(output="\n\n".join(lines)) diff --git a/tests/test_tools/test_web_search_tool.py b/tests/test_tools/test_web_search_tool.py new file mode 100644 index 0000000..2580201 --- /dev/null +++ b/tests/test_tools/test_web_search_tool.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from pathlib import Path + +import httpx +import pytest + +from leeway.tools.base import ToolExecutionContext +from leeway.tools.web_search_tool import WebSearchInput, WebSearchTool + + +class _MockResponse: + def __init__(self, payload: dict): + self._payload = payload + + def raise_for_status(self) -> None: + return None + + def json(self) -> dict: + return self._payload + + +class _MockAsyncClient: + def __init__(self, payload: dict): + self.payload = payload + self.called = [] + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return False + + async def get(self, url: str, params: dict, headers: dict): + self.called.append({"url": url, "params": params, "headers": headers}) + return _MockResponse(self.payload) + + +@pytest.mark.asyncio +async def test_web_search_you_provider_success(monkeypatch): + tool = WebSearchTool() + context = ToolExecutionContext(cwd=Path(".")) + + monkeypatch.setenv("WEB_SEARCH_PROVIDER", "you") + monkeypatch.setenv("YOU_SEARCH_API_KEY", "test-key") + + mock_client = _MockAsyncClient( + { + "hits": [ + { + "title": "Result A", + "url": "https://example.com/a", + "snippets": ["Snippet A"], + "description": "Fallback description", + } + ] + } + ) + + monkeypatch.setattr(httpx, "AsyncClient", lambda timeout=15.0: mock_client) + + result = await tool.execute(WebSearchInput(query="test", num_results=1), context) + + assert not result.is_error + assert "Result A" in result.output + assert "https://api.ydc-index.io/v1/search" in mock_client.called[0]["url"] + + +@pytest.mark.asyncio +async def test_web_search_missing_key_for_you(monkeypatch): + tool = WebSearchTool() + context = ToolExecutionContext(cwd=Path(".")) + + monkeypatch.setenv("WEB_SEARCH_PROVIDER", "you") + monkeypatch.delenv("YOU_SEARCH_API_KEY", raising=False) + + result = await tool.execute(WebSearchInput(query="test"), context) + + assert result.is_error + assert "YOU_SEARCH_API_KEY" in result.output + + +@pytest.mark.asyncio +async def test_web_search_invalid_provider(monkeypatch): + tool = WebSearchTool() + context = ToolExecutionContext(cwd=Path(".")) + + monkeypatch.setenv("WEB_SEARCH_PROVIDER", "invalid") + + result = await tool.execute(WebSearchInput(query="test"), context) + + assert result.is_error + assert "Unsupported WEB_SEARCH_PROVIDER" in result.output From 788d69d20f96a112bb892650b6ac49a3318f3a8c Mon Sep 17 00:00:00 2001 From: Mouse Date: Wed, 15 Apr 2026 12:33:10 -0700 Subject: [PATCH 2/5] fix: align you.com params and improve response parsing --- src/leeway/tools/web_search_tool.py | 2 +- tests/test_tools/test_web_search_tool.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/leeway/tools/web_search_tool.py b/src/leeway/tools/web_search_tool.py index eb0b6a3..39ee4a6 100644 --- a/src/leeway/tools/web_search_tool.py +++ b/src/leeway/tools/web_search_tool.py @@ -64,7 +64,7 @@ async def execute(self, arguments: BaseModel, context: ToolExecutionContext) -> async with httpx.AsyncClient(timeout=15.0) as client: resp = await client.get( "https://api.ydc-index.io/v1/search", - params={"query": args.query, "num_web_results": args.num_results}, + params={"query": args.query, "count": args.num_results}, headers={"X-API-Key": api_key}, ) resp.raise_for_status() diff --git a/tests/test_tools/test_web_search_tool.py b/tests/test_tools/test_web_search_tool.py index 2580201..a0131f0 100644 --- a/tests/test_tools/test_web_search_tool.py +++ b/tests/test_tools/test_web_search_tool.py @@ -64,6 +64,8 @@ async def test_web_search_you_provider_success(monkeypatch): assert not result.is_error assert "Result A" in result.output assert "https://api.ydc-index.io/v1/search" in mock_client.called[0]["url"] + assert mock_client.called[0]["params"]["query"] == "test" + assert mock_client.called[0]["params"]["count"] == 1 @pytest.mark.asyncio From c416a722c494651d7ba62193b70728680b6106fe Mon Sep 17 00:00:00 2001 From: Mouse Date: Thu, 16 Apr 2026 11:21:36 -0700 Subject: [PATCH 3/5] fix: align you.com params and add fallback request handling --- src/leeway/tools/web_search_tool.py | 12 +++++- tests/test_tools/test_web_search_tool.py | 49 ++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/leeway/tools/web_search_tool.py b/src/leeway/tools/web_search_tool.py index 39ee4a6..903f2d1 100644 --- a/src/leeway/tools/web_search_tool.py +++ b/src/leeway/tools/web_search_tool.py @@ -62,17 +62,25 @@ async def execute(self, arguments: BaseModel, context: ToolExecutionContext) -> try: async with httpx.AsyncClient(timeout=15.0) as client: + params = {"query": args.query, "num_web_results": args.num_results} resp = await client.get( "https://api.ydc-index.io/v1/search", - params={"query": args.query, "count": args.num_results}, + params=params, headers={"X-API-Key": api_key}, ) + if resp.status_code == 422: + # Compatibility fallback for endpoints expecting `count`. + resp = await client.get( + "https://api.ydc-index.io/v1/search", + params={"query": args.query, "count": args.num_results}, + headers={"X-API-Key": api_key}, + ) resp.raise_for_status() except Exception as exc: return ToolResult(output=f"you.com search failed: {exc}", is_error=True) data = resp.json() - results = data.get("hits", []) + results = data.get("hits") or data.get("results", {}).get("web", []) else: api_key = os.environ.get("BRAVE_SEARCH_API_KEY", "") if not api_key: diff --git a/tests/test_tools/test_web_search_tool.py b/tests/test_tools/test_web_search_tool.py index a0131f0..e0b2859 100644 --- a/tests/test_tools/test_web_search_tool.py +++ b/tests/test_tools/test_web_search_tool.py @@ -10,8 +10,9 @@ class _MockResponse: - def __init__(self, payload: dict): + def __init__(self, payload: dict, status_code: int = 200): self._payload = payload + self.status_code = status_code def raise_for_status(self) -> None: return None @@ -21,8 +22,9 @@ def json(self) -> dict: class _MockAsyncClient: - def __init__(self, payload: dict): - self.payload = payload + def __init__(self, payload: dict | None = None, responses: list[_MockResponse] | None = None): + self.payload = payload or {} + self.responses = responses or [] self.called = [] async def __aenter__(self): @@ -33,6 +35,8 @@ async def __aexit__(self, exc_type, exc, tb): async def get(self, url: str, params: dict, headers: dict): self.called.append({"url": url, "params": params, "headers": headers}) + if self.responses: + return self.responses.pop(0) return _MockResponse(self.payload) @@ -65,7 +69,7 @@ async def test_web_search_you_provider_success(monkeypatch): assert "Result A" in result.output assert "https://api.ydc-index.io/v1/search" in mock_client.called[0]["url"] assert mock_client.called[0]["params"]["query"] == "test" - assert mock_client.called[0]["params"]["count"] == 1 + assert mock_client.called[0]["params"]["num_web_results"] == 1 @pytest.mark.asyncio @@ -93,3 +97,40 @@ async def test_web_search_invalid_provider(monkeypatch): assert result.is_error assert "Unsupported WEB_SEARCH_PROVIDER" in result.output + + +@pytest.mark.asyncio +async def test_web_search_you_provider_fallback_to_count(monkeypatch): + tool = WebSearchTool() + context = ToolExecutionContext(cwd=Path(".")) + + monkeypatch.setenv("WEB_SEARCH_PROVIDER", "you") + monkeypatch.setenv("YOU_SEARCH_API_KEY", "test-key") + + mock_client = _MockAsyncClient( + responses=[ + _MockResponse({}, status_code=422), + _MockResponse( + { + "results": { + "web": [ + { + "title": "Result B", + "url": "https://example.com/b", + "snippets": ["Snippet B"], + } + ] + } + } + ), + ] + ) + + monkeypatch.setattr(httpx, "AsyncClient", lambda timeout=15.0: mock_client) + + result = await tool.execute(WebSearchInput(query="fallback", num_results=2), context) + + assert not result.is_error + assert "Result B" in result.output + assert mock_client.called[0]["params"] == {"query": "fallback", "num_web_results": 2} + assert mock_client.called[1]["params"] == {"query": "fallback", "count": 2} From 499cc4f3c2a10ad16bddbc253d32ae0bf34e4614 Mon Sep 17 00:00:00 2001 From: Mouse Date: Fri, 17 Apr 2026 11:57:23 -0700 Subject: [PATCH 4/5] fix: align you provider params and tests with v1 API --- README.md | 22 --------- src/leeway/tools/web_search_tool.py | 11 +---- tests/test_tools/test_web_search_tool.py | 57 +++++------------------- 3 files changed, 13 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 3535359..2ce53b8 100644 --- a/README.md +++ b/README.md @@ -162,28 +162,6 @@ See **[docs/workflows.md](docs/workflows.md)** for the full pattern catalog and --- -### Web Search Provider Setup - -`web_search` supports two providers via environment variables: - -```bash -# Default provider (backward compatible) -export WEB_SEARCH_PROVIDER=brave -export BRAVE_SEARCH_API_KEY=your_brave_key - -# Optional provider: you.com Search API -export WEB_SEARCH_PROVIDER=you -export YOU_SEARCH_API_KEY=your_you_api_key -``` - -Usage in prompts/workflows remains unchanged: - -```text -Use web_search with query: "latest model context protocol updates" -``` - ---- - ## Learn More | Topic | Docs | diff --git a/src/leeway/tools/web_search_tool.py b/src/leeway/tools/web_search_tool.py index 903f2d1..ec4c444 100644 --- a/src/leeway/tools/web_search_tool.py +++ b/src/leeway/tools/web_search_tool.py @@ -62,25 +62,18 @@ async def execute(self, arguments: BaseModel, context: ToolExecutionContext) -> try: async with httpx.AsyncClient(timeout=15.0) as client: - params = {"query": args.query, "num_web_results": args.num_results} + params = {"query": args.query, "count": args.num_results} resp = await client.get( "https://api.ydc-index.io/v1/search", params=params, headers={"X-API-Key": api_key}, ) - if resp.status_code == 422: - # Compatibility fallback for endpoints expecting `count`. - resp = await client.get( - "https://api.ydc-index.io/v1/search", - params={"query": args.query, "count": args.num_results}, - headers={"X-API-Key": api_key}, - ) resp.raise_for_status() except Exception as exc: return ToolResult(output=f"you.com search failed: {exc}", is_error=True) data = resp.json() - results = data.get("hits") or data.get("results", {}).get("web", []) + results = data.get("results", {}).get("web", []) else: api_key = os.environ.get("BRAVE_SEARCH_API_KEY", "") if not api_key: diff --git a/tests/test_tools/test_web_search_tool.py b/tests/test_tools/test_web_search_tool.py index e0b2859..ba50b87 100644 --- a/tests/test_tools/test_web_search_tool.py +++ b/tests/test_tools/test_web_search_tool.py @@ -50,14 +50,16 @@ async def test_web_search_you_provider_success(monkeypatch): mock_client = _MockAsyncClient( { - "hits": [ - { - "title": "Result A", - "url": "https://example.com/a", - "snippets": ["Snippet A"], - "description": "Fallback description", - } - ] + "results": { + "web": [ + { + "title": "Result A", + "url": "https://example.com/a", + "snippets": ["Snippet A"], + "description": "Fallback description", + } + ] + } } ) @@ -69,7 +71,7 @@ async def test_web_search_you_provider_success(monkeypatch): assert "Result A" in result.output assert "https://api.ydc-index.io/v1/search" in mock_client.called[0]["url"] assert mock_client.called[0]["params"]["query"] == "test" - assert mock_client.called[0]["params"]["num_web_results"] == 1 + assert mock_client.called[0]["params"]["count"] == 1 @pytest.mark.asyncio @@ -97,40 +99,3 @@ async def test_web_search_invalid_provider(monkeypatch): assert result.is_error assert "Unsupported WEB_SEARCH_PROVIDER" in result.output - - -@pytest.mark.asyncio -async def test_web_search_you_provider_fallback_to_count(monkeypatch): - tool = WebSearchTool() - context = ToolExecutionContext(cwd=Path(".")) - - monkeypatch.setenv("WEB_SEARCH_PROVIDER", "you") - monkeypatch.setenv("YOU_SEARCH_API_KEY", "test-key") - - mock_client = _MockAsyncClient( - responses=[ - _MockResponse({}, status_code=422), - _MockResponse( - { - "results": { - "web": [ - { - "title": "Result B", - "url": "https://example.com/b", - "snippets": ["Snippet B"], - } - ] - } - } - ), - ] - ) - - monkeypatch.setattr(httpx, "AsyncClient", lambda timeout=15.0: mock_client) - - result = await tool.execute(WebSearchInput(query="fallback", num_results=2), context) - - assert not result.is_error - assert "Result B" in result.output - assert mock_client.called[0]["params"] == {"query": "fallback", "num_web_results": 2} - assert mock_client.called[1]["params"] == {"query": "fallback", "count": 2} From 3a0992e247e4c46e4fee7eb7d4e0d661504a672a Mon Sep 17 00:00:00 2001 From: Mouse Date: Thu, 7 May 2026 11:22:52 -0700 Subject: [PATCH 5/5] fix: use YDC_API_KEY and support keyless you search --- docs/tools.md | 2 +- src/leeway/tools/web_search_tool.py | 18 ++++++---------- tests/test_tools/test_web_search_tool.py | 26 +++++++++++++++++++----- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/docs/tools.md b/docs/tools.md index cfd86c0..2ac089a 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -24,7 +24,7 @@ export BRAVE_SEARCH_API_KEY=your_brave_key # Optional provider: you.com Search API export WEB_SEARCH_PROVIDER=you -export YOU_SEARCH_API_KEY=your_you_api_key +export YDC_API_KEY=your_you_api_key ``` Usage in prompts/workflows remains unchanged: diff --git a/src/leeway/tools/web_search_tool.py b/src/leeway/tools/web_search_tool.py index ec4c444..e52da3d 100644 --- a/src/leeway/tools/web_search_tool.py +++ b/src/leeway/tools/web_search_tool.py @@ -21,7 +21,8 @@ class WebSearchTool(BaseTool): Provider selection: - ``WEB_SEARCH_PROVIDER=brave`` (default) requires ``BRAVE_SEARCH_API_KEY`` - - ``WEB_SEARCH_PROVIDER=you`` requires ``YOU_SEARCH_API_KEY`` + - ``WEB_SEARCH_PROVIDER=you`` supports free-tier unauthenticated search, + or set ``YDC_API_KEY`` for authenticated usage """ name = "web_search" @@ -50,23 +51,16 @@ async def execute(self, arguments: BaseModel, context: ToolExecutionContext) -> ) if provider == "you": - api_key = os.environ.get("YOU_SEARCH_API_KEY", "") - if not api_key: - return ToolResult( - output=( - "No API key found for you.com search. Set YOU_SEARCH_API_KEY " - "or switch WEB_SEARCH_PROVIDER=brave." - ), - is_error=True, - ) + api_key = os.environ.get("YDC_API_KEY", "") try: async with httpx.AsyncClient(timeout=15.0) as client: params = {"query": args.query, "count": args.num_results} + headers = {"X-API-Key": api_key} if api_key else {} resp = await client.get( "https://api.ydc-index.io/v1/search", params=params, - headers={"X-API-Key": api_key}, + headers=headers, ) resp.raise_for_status() except Exception as exc: @@ -80,7 +74,7 @@ async def execute(self, arguments: BaseModel, context: ToolExecutionContext) -> return ToolResult( output=( "No API key found for Brave search. Set BRAVE_SEARCH_API_KEY " - "or switch WEB_SEARCH_PROVIDER=you with YOU_SEARCH_API_KEY." + "or switch WEB_SEARCH_PROVIDER=you (optionally set YDC_API_KEY)." ), is_error=True, ) diff --git a/tests/test_tools/test_web_search_tool.py b/tests/test_tools/test_web_search_tool.py index ba50b87..7059dc0 100644 --- a/tests/test_tools/test_web_search_tool.py +++ b/tests/test_tools/test_web_search_tool.py @@ -46,7 +46,7 @@ async def test_web_search_you_provider_success(monkeypatch): context = ToolExecutionContext(cwd=Path(".")) monkeypatch.setenv("WEB_SEARCH_PROVIDER", "you") - monkeypatch.setenv("YOU_SEARCH_API_KEY", "test-key") + monkeypatch.setenv("YDC_API_KEY", "test-key") mock_client = _MockAsyncClient( { @@ -75,17 +75,33 @@ async def test_web_search_you_provider_success(monkeypatch): @pytest.mark.asyncio -async def test_web_search_missing_key_for_you(monkeypatch): +async def test_web_search_you_provider_without_key(monkeypatch): tool = WebSearchTool() context = ToolExecutionContext(cwd=Path(".")) monkeypatch.setenv("WEB_SEARCH_PROVIDER", "you") - monkeypatch.delenv("YOU_SEARCH_API_KEY", raising=False) + monkeypatch.delenv("YDC_API_KEY", raising=False) + + mock_client = _MockAsyncClient( + { + "results": { + "web": [ + { + "title": "Result A", + "url": "https://example.com/a", + "description": "Fallback description", + } + ] + } + } + ) + monkeypatch.setattr(httpx, "AsyncClient", lambda timeout=15.0: mock_client) result = await tool.execute(WebSearchInput(query="test"), context) - assert result.is_error - assert "YOU_SEARCH_API_KEY" in result.output + assert not result.is_error + assert "Result A" in result.output + assert "X-API-Key" not in mock_client.called[0]["headers"] @pytest.mark.asyncio