Files
devplacepy/tests/unit/services/jobs/deepsearch/crawl.py
retoorandClaude Sonnet 5.5 02ab747cd0 Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
Continuous Delivery could never be turned off once a crop was planted. Add a
one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool
game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant,
autoreplant claim or Refactor. Autoreplant now also respects crop locks through
the shared _crop_lock predicate. Plan and measurements in happyplanting.md.

Also includes the pending community sync, node network and related work in the
tree; the full suite passed (4999) with all of it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 02:23:43 +02:00

224 lines
8.1 KiB
Python

# retoor <retoor@molodetz.nl>
from tests.conftest import run_async
from devplacepy.services.jobs.deepsearch import crawl as crawl_module
from devplacepy.services.jobs.deepsearch.crawl import (
CrawledPage,
_interleave,
_is_hostile,
_snippet_page,
crawl,
)
async def _no_stop() -> bool:
return False
def test_interleave_round_robins_and_dedupes():
buckets = [
[{"url": "a"}, {"url": "b"}],
[{"url": "c"}, {"url": "a"}],
[{"url": "d"}],
]
assert [item["url"] for item in _interleave(buckets)] == ["a", "c", "d", "b"]
def test_is_hostile_matches_social_domains():
assert _is_hostile("https://x.com/user/status/1")
assert _is_hostile("https://www.youtube.com/watch?v=abc")
assert _is_hostile("https://old.reddit.com/r/x")
assert not _is_hostile("https://blog.example.com/post")
def test_snippet_page_prefers_content_over_description():
candidate = {
"url": "https://x.com/a/status/1",
"title": "Tweet",
"description": "short",
"content": "This is the full rsearch content, deliberately written long enough to clear the snippet minimum length floor so that it is kept as a real source easily.",
}
page = _snippet_page(candidate, 0)
assert page is not None
assert page.source == "search"
assert "full rsearch content" in page.text
def test_snippet_page_none_when_too_thin():
assert _snippet_page({"url": "https://x.com/a", "content": "tiny"}, 0) is None
def test_crawl_uses_snippet_for_hostile_and_skips_fetch(monkeypatch):
fetched = []
async def fake_fetch(url, depth, browser=None):
fetched.append(url)
return CrawledPage(url=url, title="T", text="x " * 200, source="httpx", status=200, depth=depth)
monkeypatch.setattr(crawl_module, "fetch_page", fake_fetch)
candidates = [
{
"url": "https://x.com/ThePrimeagen/status/1",
"title": "Prime",
"description": "",
"content": "The real tweet text returned by rsearch for this social post, well past the snippet minimum length threshold so it survives as a usable source here.",
}
]
outcome = run_async(
crawl(candidates, 6, lambda f: None, lambda u: False, _no_stop, query="q", depth=1)
)
assert fetched == []
assert len(outcome.pages) == 1
assert outcome.pages[0].source == "search"
def test_crawl_prefers_richer_crawl_over_snippet(monkeypatch):
async def fake_fetch(url, depth, browser=None):
return CrawledPage(url=url, title="Article", text="Deep article body. " * 60, source="httpx", status=200, depth=depth)
monkeypatch.setattr(crawl_module, "fetch_page", fake_fetch)
candidates = [
{
"url": "https://blog.example.com/a",
"title": "Blog",
"description": "",
"content": "A short snippet that is longer than the floor but shorter than the crawled article body itself.",
}
]
outcome = run_async(
crawl(candidates, 6, lambda f: None, lambda u: False, _no_stop, query="q", depth=1)
)
assert outcome.pages[0].source == "httpx"
def test_crawl_falls_back_to_snippet_when_fetch_empty(monkeypatch):
async def fake_fetch(url, depth, browser=None):
return None
monkeypatch.setattr(crawl_module, "fetch_page", fake_fetch)
candidates = [
{
"url": "https://walled.example.com/a",
"title": "Wall",
"description": "",
"content": "The search snippet holds the real content that the login-walled page refused to serve to the bot today, and it is comfortably past the snippet minimum length floor.",
}
]
outcome = run_async(
crawl(candidates, 6, lambda f: None, lambda u: False, _no_stop, query="q", depth=1)
)
assert len(outcome.pages) == 1
assert outcome.pages[0].source == "search"
def test_crawl_degrades_to_httpx_when_the_playwright_driver_cannot_start(monkeypatch):
import playwright.async_api as playwright_api
class FailingDriver:
async def start(self):
raise RuntimeError("driver missing")
monkeypatch.setattr(playwright_api, "async_playwright", lambda: FailingDriver())
async def fake_fetch(url, depth, browser=None):
return CrawledPage(
url=url, title="T", text="body " * 100, source="httpx", status=200, depth=depth
)
monkeypatch.setattr(crawl_module, "fetch_page", fake_fetch)
candidates = [
{"url": "https://example.com/a", "title": "A", "description": "", "content": ""}
]
outcome = run_async(
crawl(candidates, 6, lambda f: None, lambda u: False, _no_stop, query="q", depth=1)
)
assert len(outcome.pages) == 1
assert outcome.pages[0].source == "httpx"
REDDIT_URL = "https://www.reddit.com/r/python/comments/abc/post/"
REDDIT_HTML = "<html><head><title>A thread</title></head><body><p>" + "Real thread text from a donor. " * 30 + "</p></body></html>"
REDDIT_CANDIDATE = {"url": REDDIT_URL, "title": "t", "description": "", "content": "snippet " * 30}
def _patch_node_client(monkeypatch, handler):
import httpx
real = crawl_module.stealth.stealth_async_client
def factory(**kwargs):
kwargs.pop("transport", None)
return real(transport=httpx.MockTransport(handler), **kwargs)
monkeypatch.setattr(crawl_module.stealth, "stealth_async_client", factory)
def test_node_servable_is_exactly_the_node_allowlist():
assert crawl_module.node_servable(REDDIT_URL)
assert crawl_module.node_servable("https://redd.it/abc")
assert not crawl_module.node_servable("https://x.com/a/status/1")
assert not crawl_module.node_servable("https://evilreddit.com/")
assert not crawl_module.node_servable("https://reddit.com:22/")
def test_a_hostile_reddit_page_is_fetched_through_the_nodes_when_a_key_is_present(monkeypatch):
seen = []
def handler(request):
import httpx
seen.append((request.url.path, request.headers.get("authorization"), request.content.decode()))
return httpx.Response(200, json={"url": REDDIT_URL, "status": 200, "via": "tunnel", "text": REDDIT_HTML})
_patch_node_client(monkeypatch, handler)
page = run_async(crawl_module._resolve_candidate(REDDIT_CANDIDATE, 1, None, "key123"))
assert page.source == "node-tunnel" and page.title == "A thread" and "Real thread text" in page.text
assert seen[0][0] == "/nodes/internal/fetch" and seen[0][1] == "Bearer key123" and "reddit.com" in seen[0][2]
def test_without_a_key_or_when_the_nodes_refuse_the_snippet_is_used(monkeypatch):
def handler(request):
import httpx
return httpx.Response(503, json={"error": {"reason": "no_nodes"}})
_patch_node_client(monkeypatch, handler)
assert run_async(crawl_module._resolve_candidate(REDDIT_CANDIDATE, 1, None, "")).source == "search"
assert run_async(crawl_module._resolve_candidate(REDDIT_CANDIDATE, 1, None, "key")).source == "search"
def test_a_blocked_donor_answer_falls_back_to_the_snippet(monkeypatch):
def handler(request):
import httpx
return httpx.Response(200, json={"url": REDDIT_URL, "status": 403, "via": "fetch", "text": "blocked " * 100})
_patch_node_client(monkeypatch, handler)
assert run_async(crawl_module._resolve_candidate(REDDIT_CANDIDATE, 1, None, "key")).source == "search"
def test_other_hostile_hosts_never_reach_the_nodes(monkeypatch):
calls = []
def handler(request):
import httpx
calls.append(request.url)
return httpx.Response(200, json={})
_patch_node_client(monkeypatch, handler)
candidate = {"url": "https://x.com/a/status/1", "title": "t", "description": "", "content": "snippet " * 30}
assert run_async(crawl_module._resolve_candidate(candidate, 1, None, "key")).source == "search"
assert calls == []
def test_a_thin_node_page_is_rejected(monkeypatch):
def handler(request):
import httpx
return httpx.Response(200, json={"url": REDDIT_URL, "status": 200, "via": "fetch", "text": "<html><body>tiny</body></html>"})
_patch_node_client(monkeypatch, handler)
assert run_async(crawl_module.node_page(REDDIT_URL, 1, "key")) is None