Skip to content

Commit ecf2cc0

Browse files
test(agent/tools): cover Crawler._invoke runtime path
Address review feedback on #16415: add direct regression tests for the restored execution path (not just CrawlerParam). Using a mocked crawl result and stubbed SSRF guard: - _invoke(query=...) returns the page content and writes it to formalized_content. - an empty query short-circuits without crawling. - an unsafe URL is rejected ("URL not valid" / _ERROR) before any crawl.
1 parent b04f54c commit ecf2cc0

1 file changed

Lines changed: 75 additions & 1 deletion

File tree

test/unit_test/agent/component/test_crawler.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,26 @@
1414
# limitations under the License.
1515
#
1616

17+
import contextlib
18+
1719
import pytest
1820

1921
# Crawler imports the `crawl4ai` SDK at module load; skip where absent.
2022
pytest.importorskip("crawl4ai")
2123

22-
from agent.tools.crawler import CrawlerParam # noqa: E402
24+
from agent.tools.crawler import Crawler, CrawlerParam # noqa: E402
25+
26+
27+
def _make_tool():
28+
# Bypass the canvas-bound __init__ (mirrors test_pubmed_unit.py) and stub the
29+
# canvas-touching helpers so we can exercise _invoke's execution path.
30+
crawler = Crawler.__new__(Crawler)
31+
crawler._param = CrawlerParam()
32+
crawler.check_if_canceled = lambda *a, **k: False
33+
out = {}
34+
crawler.set_output = lambda k, v: out.__setitem__(k, v)
35+
crawler.output = lambda k=None: (out.get(k) if k else out)
36+
return crawler, out
2337

2438

2539
def test_param_instantiates():
@@ -48,3 +62,63 @@ def test_check_rejects_invalid_extract_type():
4862
param.extract_type = "pdf"
4963
with pytest.raises(ValueError):
5064
param.check()
65+
66+
67+
def test_invoke_returns_content_and_sets_formalized_content(monkeypatch):
68+
# Regression for the restored runtime path: _invoke(query=...) must fetch
69+
# the page, return its content, and write it to formalized_content.
70+
import common.ssrf_guard as ssrf
71+
72+
monkeypatch.setattr(ssrf, "assert_url_is_safe", lambda url: ("example.com", "93.184.216.34"))
73+
monkeypatch.setattr(ssrf, "pin_dns_global", lambda *a, **k: contextlib.nullcontext())
74+
75+
crawler, out = _make_tool()
76+
77+
async def fake_get_web(url):
78+
return "PAGE CONTENT for " + url
79+
80+
crawler.get_web = fake_get_web
81+
82+
result = crawler._invoke(query="http://example.com")
83+
84+
assert result == "PAGE CONTENT for http://example.com"
85+
assert out["formalized_content"] == "PAGE CONTENT for http://example.com"
86+
87+
88+
def test_invoke_empty_query_returns_empty():
89+
# Empty query short-circuits without crawling.
90+
crawler, out = _make_tool()
91+
called = []
92+
93+
async def fake_get_web(url):
94+
called.append(url)
95+
return "should not be used"
96+
97+
crawler.get_web = fake_get_web
98+
99+
assert crawler._invoke(query="") == ""
100+
assert out.get("formalized_content") == ""
101+
assert called == []
102+
103+
104+
def test_invoke_rejects_unsafe_url(monkeypatch):
105+
# An unsafe URL is rejected before any crawl is attempted.
106+
import common.ssrf_guard as ssrf
107+
108+
def _reject(url):
109+
raise ValueError("blocked")
110+
111+
monkeypatch.setattr(ssrf, "assert_url_is_safe", _reject)
112+
113+
crawler, out = _make_tool()
114+
called = []
115+
116+
async def fake_get_web(url):
117+
called.append(url)
118+
return "should not be used"
119+
120+
crawler.get_web = fake_get_web
121+
122+
assert crawler._invoke(query="http://169.254.169.254/") == "URL not valid"
123+
assert out.get("_ERROR") == "URL not valid"
124+
assert called == []

0 commit comments

Comments
 (0)