Spaces:
Running
Running
| """No-network guards for the Tavily image search. | |
| The feature is best-effort by contract: every failure path must return an | |
| empty list rather than raise, because an image lookup must never be able to | |
| break or delay an answer. | |
| """ | |
| import httpx | |
| import pytest | |
| from app import websearch | |
| from app.config import settings | |
| from app.websearch import _parse, search_images, search_subject | |
| PAYLOAD = { | |
| "images": [ | |
| {"url": "https://a.example/1.jpg", "description": "a photo"}, | |
| {"url": "https://a.example/1.jpg", "description": "dupe"}, | |
| {"url": "", "description": "no url"}, | |
| {"url": "ftp://a.example/3.jpg", "description": "wrong scheme"}, | |
| # plain http would be blocked as mixed content on the TLS deployment | |
| {"url": "http://a.example/5.jpg", "description": "insecure"}, | |
| {"url": "https://a.example/2.jpg", "description": "x" * 500}, | |
| "https://a.example/4.jpg", # bare string (no image descriptions) | |
| ], | |
| } | |
| def test_parse_filters_and_dedupes(): | |
| out = _parse(PAYLOAD, limit=10) | |
| assert [i.url for i in out] == [ | |
| "https://a.example/1.jpg", | |
| "https://a.example/2.jpg", | |
| "https://a.example/4.jpg", | |
| ] | |
| assert len(out[1].description) == 200 # truncated | |
| assert out[2].description == "" # bare string -> no description | |
| def test_parse_respects_limit(): | |
| assert len(_parse(PAYLOAD, limit=2)) == 2 | |
| def test_parse_tolerates_missing_key(): | |
| assert _parse({}, limit=3) == [] | |
| def test_search_subject(query, expected): | |
| assert search_subject(query) == expected | |
| def enabled(monkeypatch): | |
| monkeypatch.setattr(settings, "tavily_api_key", "tvly-test") | |
| monkeypatch.setattr(settings, "image_search_enabled", True) | |
| def _stub(monkeypatch, handler): | |
| """Point websearch's module client at a mock transport.""" | |
| client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) | |
| monkeypatch.setattr(websearch, "_client", client) | |
| async def test_search_images_happy_path(monkeypatch, enabled): | |
| _stub(monkeypatch, lambda req: httpx.Response(200, json=PAYLOAD)) | |
| out = await search_images("mount fuji", limit=3) | |
| assert len(out) == 3 | |
| assert out[0].url == "https://a.example/1.jpg" | |
| async def test_search_images_swallows_http_error(monkeypatch, enabled): | |
| _stub(monkeypatch, lambda req: httpx.Response(500, text="boom")) | |
| assert await search_images("mount fuji") == [] | |
| async def test_search_images_swallows_timeout(monkeypatch, enabled): | |
| def handler(req): | |
| raise httpx.ReadTimeout("too slow", request=req) | |
| _stub(monkeypatch, handler) | |
| assert await search_images("mount fuji") == [] | |
| async def test_search_images_swallows_bad_json(monkeypatch, enabled): | |
| _stub(monkeypatch, lambda req: httpx.Response(200, text="not json")) | |
| assert await search_images("mount fuji") == [] | |
| async def test_disabled_without_key(monkeypatch): | |
| monkeypatch.setattr(settings, "tavily_api_key", "") | |
| # No transport stub: a real call would fail the test by hanging/erroring | |
| assert await search_images("mount fuji") == [] | |
| assert not websearch.enabled() | |
| async def test_empty_query_short_circuits(monkeypatch, enabled): | |
| assert await search_images(" ") == [] | |