From a82a94ffa7aa773cbea7a56aa87e4b15f37d7fed Mon Sep 17 00:00:00 2001 From: Nick Sweeting Date: Sat, 18 Jul 2026 15:39:22 -0700 Subject: [PATCH] Make recursive crawl coverage deterministic --- archivebox/tests/conftest.py | 30 ++++++++++++---- archivebox/tests/test_recursive_crawl.py | 46 ++++++++++++++++++++---- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/archivebox/tests/conftest.py b/archivebox/tests/conftest.py index bf01a318..f9e413cc 100644 --- a/archivebox/tests/conftest.py +++ b/archivebox/tests/conftest.py @@ -436,27 +436,45 @@ def recursive_test_site(): "/about": """ - Deep About +
+
+

About

+

This local article describes the ArchiveBox recursive crawl fixture with enough semantic content for real article extractors.

+ Deep About +
+
""".strip().encode("utf-8"), "/blog": """ - Deep Blog +
+
+

Blog

+

This local article exercises real browser, text extraction, and recursive discovery behavior without depending on a public website.

+ Deep Blog +
+
""".strip().encode("utf-8"), "/contact": """ - Deep Contact +
+
+

Contact

+

This local article keeps the deterministic crawl graph content-rich while preserving the original contact and deep-contact URLs.

+ Deep Contact +
+
""".strip().encode("utf-8"), - "/deep/about": b"

Deep About

", - "/deep/blog": b"

Deep Blog

", - "/deep/contact": b"

Deep Contact

", + "/deep/about": b"

Deep About

This depth-two article verifies that recursive snapshots retain extractable semantic content and their exact parent relationship.

", + "/deep/blog": b"

Deep Blog

This depth-two article provides deterministic local content for every real extraction plugin in the recursive crawl.

", + "/deep/contact": b"

Deep Contact

This depth-two article completes the fixed local graph without relying on mutable third-party pages or network policy.

", "/favicon.ico": b"test-icon", } diff --git a/archivebox/tests/test_recursive_crawl.py b/archivebox/tests/test_recursive_crawl.py index 6ad69afa..23bc6a17 100644 --- a/archivebox/tests/test_recursive_crawl.py +++ b/archivebox/tests/test_recursive_crawl.py @@ -519,12 +519,16 @@ def test_add_archivewebpage_installs_required_chrome_dependency(initialized_arch @pytest.mark.timeout(1200) -def test_recursive_crawl_depth_two_all_plugins_runs_snapshots_in_parallel(initialized_archive, free_tcp_port_factory): +def test_recursive_crawl_depth_two_all_plugins_runs_snapshots_in_parallel( + initialized_archive, + free_tcp_port_factory, + recursive_test_site, +): """Run a bounded real depth=2 crawl with all plugins enabled and verify parallel snapshot execution.""" from abx_dl.models import discover_plugins - root_url = "https://example.com/" + root_url = recursive_test_site["root_url"] plugin_selection = ",".join(sorted(plugin for plugin in discover_plugins().keys() if not plugin.startswith("claude"))) env = os.environ.copy() for preinstalled_path_key in ( @@ -542,6 +546,7 @@ def test_recursive_crawl_depth_two_all_plugins_runs_snapshots_in_parallel(initia { "USE_COLOR": "false", "SHOW_PROGRESS": "false", + "URL_ALLOWLIST": r"127\.0\.0\.1[:/].*", "ABXPKG_LIB_DIR": str(initialized_archive / "lib"), "CHROMEWEBSTORE_EXTENSIONS_DIR": str(initialized_archive / "lib/chromewebstore/extensions"), "TIMEOUT": "90", @@ -621,10 +626,26 @@ def test_recursive_crawl_depth_two_all_plugins_runs_snapshots_in_parallel(initia assert crawl.status == Crawl.StatusChoices.SEALED assert crawl.retry_at is None - assert len(snapshots) == 8 - assert any(url == root_url and depth == 0 for _id, url, depth, _status, _parent, _downloaded_at in snapshots) - assert any("iana.org" in url and depth == 1 for _id, url, depth, _status, _parent, _downloaded_at in snapshots) - assert any(depth == 2 for _id, _url, depth, _status, _parent, _downloaded_at in snapshots) + expected_urls = { + root_url, + *recursive_test_site["child_urls"], + *recursive_test_site["deep_urls"], + } + snapshots_by_url = {url: (snapshot_id, depth, parent_id) for snapshot_id, url, depth, _status, parent_id, _downloaded_at in snapshots} + assert set(snapshots_by_url) == expected_urls + root_id, root_depth, root_parent_id = snapshots_by_url[root_url] + assert root_depth == 0 + assert root_parent_id is None + child_ids_by_url = {} + for child_url in recursive_test_site["child_urls"]: + child_id, child_depth, child_parent_id = snapshots_by_url[child_url] + assert child_depth == 1 + assert child_parent_id == root_id + child_ids_by_url[child_url] = child_id + for child_url, deep_url in zip(recursive_test_site["child_urls"], recursive_test_site["deep_urls"], strict=True): + _deep_id, deep_depth, deep_parent_id = snapshots_by_url[deep_url] + assert deep_depth == 2 + assert deep_parent_id == child_ids_by_url[child_url] assert all(status == Snapshot.StatusChoices.SEALED for _id, _url, _depth, status, _parent, _downloaded_at in snapshots) assert all(downloaded_at is not None for _id, _url, _depth, _status, _parent, downloaded_at in snapshots) @@ -647,6 +668,13 @@ def test_recursive_crawl_depth_two_all_plugins_runs_snapshots_in_parallel(initia if not (status in allowed_statuses or (plugin == "archivedotorg" and status == ArchiveResult.StatusChoices.FAILED)) ] assert not unexpected_results + ytdlp_results = [ + (url, status, output_str) + for _snapshot_id, url, _depth, plugin, _hook_name, status, _files, _size, output_str in archive_results + if plugin == "ytdlp" + ] + assert ytdlp_results + assert all(status != ArchiveResult.StatusChoices.FAILED for _url, status, _output_str in ytdlp_results), ytdlp_results plugins_seen = {plugin for _snapshot_id, _url, _depth, plugin, _hook_name, _status, _files, _size, _output in archive_results} assert { @@ -679,7 +707,11 @@ def test_recursive_crawl_depth_two_all_plugins_runs_snapshots_in_parallel(initia assert list(snapshot_root.rglob("favicon/**/*")) urls_jsonl_files = list(snapshot_root.rglob("parse_html_urls/urls.jsonl")) assert urls_jsonl_files - assert any("iana.org" in path.read_text(errors="ignore") for path in urls_jsonl_files) + parsed_urls = { + json.loads(line)["url"] for path in urls_jsonl_files for line in path.read_text(errors="ignore").splitlines() if line.strip() + } + assert set(recursive_test_site["child_urls"]).issubset(parsed_urls) + assert set(recursive_test_site["deep_urls"]).issubset(parsed_urls) assert processes failed_hook_results = [