|
14 | 14 | # limitations under the License. |
15 | 15 | # |
16 | 16 |
|
| 17 | +import contextlib |
| 18 | + |
17 | 19 | import pytest |
18 | 20 |
|
19 | 21 | # Crawler imports the `crawl4ai` SDK at module load; skip where absent. |
20 | 22 | pytest.importorskip("crawl4ai") |
21 | 23 |
|
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 |
23 | 37 |
|
24 | 38 |
|
25 | 39 | def test_param_instantiates(): |
@@ -48,3 +62,63 @@ def test_check_rejects_invalid_extract_type(): |
48 | 62 | param.extract_type = "pdf" |
49 | 63 | with pytest.raises(ValueError): |
50 | 64 | 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