from __future__ import annotations import json from svc.config.runtime_paths import runtime_skills_root from runtime.tools.public.web_search_fast_tool import _ENGINE_URLS, _normalize_engine_name, web_search_fast_tool class _Resp: def __init__(self, body: str): self._body = body.encode("utf-8") def read(self) -> bytes: return self._body def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False def test_web_search_fast_api_success(monkeypatch) -> None: payload = { "Heading": "Python", "AbstractText": "Python is a programming language.", "AbstractURL": "https://www.python.org/", "RelatedTopics": [ {"Text": "PyPI - Python packages", "FirstURL": "https://pypi.org/"}, ], } monkeypatch.setattr( "urllib.request.urlopen", lambda req, timeout=0: _Resp(json.dumps(payload)), # noqa: ARG005 ) out = web_search_fast_tool().handler({"query": "python", "provider": "ddg_api"}) assert out.get("ok") is True, out assert out.get("provider") == "ddg_api" assert int(out.get("count") or 0) >= 1 def test_web_search_fast_fallback_to_html(monkeypatch) -> None: html = """ Result A Snippet A Result B Snippet B """ state = {"n": 0} def _fake_open(req, timeout=0): # noqa: ARG001 state["n"] += 1 return _Resp(html) monkeypatch.setattr("urllib.request.urlopen", _fake_open) out = web_search_fast_tool().handler({"query": "x", "provider": "auto"}) assert out.get("ok") is True, out assert out.get("provider") == "ddg_html" assert int(out.get("count") or 0) >= 1 attempts = list(out.get("provider_attempts") or []) assert attempts and attempts[0].get("provider") == "ddg_html" def test_web_search_fast_fallback_to_bing_when_ddg_unavailable(monkeypatch) -> None: ddg_html_no_results = "
blocked
" bing_html = """
  • News 1

    Snippet 1

  • News 2

    Snippet 2

  • """ state = {"n": 0} def _fake_open(req, timeout=0): # noqa: ARG001 state["n"] += 1 if state["n"] == 1: return _Resp(ddg_html_no_results) if state["n"] == 2: return _Resp(bing_html) raise RuntimeError("unexpected_call") monkeypatch.setattr("urllib.request.urlopen", _fake_open) out = web_search_fast_tool().handler({"query": "iran news", "provider": "auto"}) assert out.get("ok") is True, out assert out.get("provider") == "bing_html" assert int(out.get("count") or 0) >= 1 attempts = list(out.get("provider_attempts") or []) assert [x.get("provider") for x in attempts[:2]] == ["ddg_html", "bing_html"] def test_web_search_fast_supports_engine_and_site(monkeypatch) -> None: captured = {"url": ""} bing_html = """
  • News 1

    Snippet 1

  • """ def _fake_open(req, timeout=0): # noqa: ARG001 captured["url"] = str(getattr(req, "full_url", "") or "") return _Resp(bing_html) monkeypatch.setattr("urllib.request.urlopen", _fake_open) out = web_search_fast_tool().handler({"query": "iran", "engine": "bing", "site": "reuters.com"}) assert out.get("ok") is True, out assert str(out.get("provider") or "").startswith("engine:") assert "site%3Areuters.com+iran" in captured["url"] or "site:reuters.com+iran" in captured["url"] def test_web_search_fast_engine_alias_from_skill_config_name(monkeypatch) -> None: captured = {"url": ""} bing_html = """
  • A

    S

  • """ def _fake_open(req, timeout=0): # noqa: ARG001 captured["url"] = str(getattr(req, "full_url", "") or "") return _Resp(bing_html) monkeypatch.setattr("urllib.request.urlopen", _fake_open) out = web_search_fast_tool().handler({"query": "x", "engine": "Bing CN"}) assert out.get("ok") is True, out assert out.get("provider") == "engine:bing_cn" assert "cn.bing.com/search" in captured["url"] def test_web_search_fast_covers_all_multi_search_engine_config_names() -> None: config_path = ( runtime_skills_root() / "_workspace" / "generalist" / "multi-search-engine" / "config.json" ) cfg = json.loads(config_path.read_text(encoding="utf-8")) names = [str(x.get("name") or "").strip() for x in list(cfg.get("engines") or [])] assert names, "multi-search-engine config engines should not be empty" unsupported: list[str] = [] for raw_name in names: norm = _normalize_engine_name(raw_name) if norm not in _ENGINE_URLS: unsupported.append(raw_name) assert not unsupported, f"unmapped engine names in config: {unsupported}" def test_web_search_fast_failure_reports_category_and_attempts(monkeypatch) -> None: def _fake_open(req, timeout=0): # noqa: ARG001 raise RuntimeError("ssrf blocked by policy") monkeypatch.setattr("urllib.request.urlopen", _fake_open) out = web_search_fast_tool().handler({"query": "iran latest", "provider": "auto"}) assert out.get("ok") is False, out assert out.get("error_code") == "search_failed" assert out.get("error_category") == "ssrf_blocked" attempts = list(out.get("provider_attempts") or []) assert attempts and attempts[0].get("provider") == "ddg_html" assert all("elapsed_ms" in x for x in attempts) def test_web_search_fast_official_api_success(monkeypatch) -> None: payload = { "webPages": { "value": [ {"name": "N1", "url": "https://example.com/n1", "snippet": "S1"}, {"name": "N2", "url": "https://example.com/n2", "snippet": "S2"}, ] } } monkeypatch.setenv("OCLAW_WEB_SEARCH_BING_API_KEY", "k") monkeypatch.setenv("OCLAW_WEB_SEARCH_BING_API_ENDPOINT", "https://api.bing.microsoft.com/v7.0/search") monkeypatch.setattr("urllib.request.urlopen", lambda req, timeout=0: _Resp(json.dumps(payload))) # noqa: ARG005 out = web_search_fast_tool().handler({"query": "ai", "provider": "official_api"}) assert out.get("ok") is True, out assert out.get("provider") == "official_bing" assert int(out.get("count") or 0) == 2 def test_web_search_fast_official_api_missing_key(monkeypatch) -> None: monkeypatch.delenv("OCLAW_WEB_SEARCH_BING_API_KEY", raising=False) monkeypatch.delenv("OCLAW_WEB_SEARCH_OFFICIAL_API_KEY", raising=False) monkeypatch.delenv("OCLAW_WEB_SEARCH_GOOGLE_API_KEY", raising=False) monkeypatch.delenv("OCLAW_WEB_SEARCH_GOOGLE_CSE_ID", raising=False) out = web_search_fast_tool().handler({"query": "ai", "provider": "official_api"}) assert out.get("ok") is False, out assert out.get("error_code") == "search_failed" assert out.get("error_category") == "auth_failed" attempts = list(out.get("provider_attempts") or []) assert attempts and attempts[0].get("provider") == "official_bing" def test_web_search_fast_auto_prefers_official_api_when_configured(monkeypatch) -> None: state = {"n": 0} payload = {"webPages": {"value": [{"name": "N", "url": "https://example.com/n", "snippet": "S"}]}} def _fake_open(req, timeout=0): # noqa: ARG001 state["n"] += 1 return _Resp(json.dumps(payload)) monkeypatch.setenv("OCLAW_WEB_SEARCH_BING_API_KEY", "k") monkeypatch.setattr("urllib.request.urlopen", _fake_open) out = web_search_fast_tool().handler({"query": "ai", "provider": "auto"}) assert out.get("ok") is True, out assert out.get("provider") == "official_bing" attempts = list(out.get("provider_attempts") or []) assert attempts and attempts[0].get("provider") == "official_bing" assert state["n"] == 1 def test_web_search_fast_official_google_success(monkeypatch) -> None: payload = { "items": [ {"title": "G1", "link": "https://example.com/g1", "snippet": "GS1"}, {"title": "G2", "link": "https://example.com/g2", "snippet": "GS2"}, ] } monkeypatch.setenv("OCLAW_WEB_SEARCH_GOOGLE_API_KEY", "gk") monkeypatch.setenv("OCLAW_WEB_SEARCH_GOOGLE_CSE_ID", "cx") monkeypatch.setattr("urllib.request.urlopen", lambda req, timeout=0: _Resp(json.dumps(payload))) # noqa: ARG005 out = web_search_fast_tool().handler({"query": "ai", "provider": "official_google"}) assert out.get("ok") is True, out assert out.get("provider") == "official_google" assert int(out.get("count") or 0) == 2 def test_web_search_fast_auto_can_prefer_google(monkeypatch) -> None: payload = {"items": [{"title": "G1", "link": "https://example.com/g1", "snippet": "GS1"}]} monkeypatch.setenv("OCLAW_WEB_SEARCH_GOOGLE_API_KEY", "gk") monkeypatch.setenv("OCLAW_WEB_SEARCH_GOOGLE_CSE_ID", "cx") monkeypatch.setattr("urllib.request.urlopen", lambda req, timeout=0: _Resp(json.dumps(payload))) # noqa: ARG005 out = web_search_fast_tool().handler({"query": "ai", "provider": "auto", "official_provider": "google"}) assert out.get("ok") is True, out assert out.get("provider") == "official_google"