mirror of
https://github.com/lfnovo/open-notebook.git
synced 2026-08-03 20:24:02 +00:00
Some checks are pending
Development Build / extract-version (push) Waiting to run
Development Build / changes (push) Waiting to run
Tests / Frontend Lint (push) Waiting to run
Tests / Backend Tests (push) Waiting to run
Tests / Backend Lint (push) Waiting to run
Tests / Backend Typecheck (push) Waiting to run
Development Build / build-regular (push) Blocked by required conditions
Development Build / build-single (push) Blocked by required conditions
Development Build / summary (push) Blocked by required conditions
Tests / Frontend Tests (push) Waiting to run
Tests / Frontend Build (push) Waiting to run
* fix(sources): fall back to auto when a selected engine's runtime is absent The content-processing engine choice is persisted in the database; the runtime that serves it (Docling, local Crawl4AI) is installed on demand from environment flags evaluated at boot. The two therefore drift: a redeploy that drops OPEN_NOTEBOOK_ENABLE_CRAWL4AI/_DOCLING, a volume moved to a new deployment, or a failed on-demand install all leave a stored selection pointing at a runtime that is not there. The source graph passed that selection straight to content-core, so every affected extraction failed with "Could not extract any text content from this source" - no mention of the engine, the runtime, or the flag that would fix it. For a URL engine set to crawl4ai this breaks URL ingestion entirely. The graph now checks runtime availability before honoring the stored engine and degrades to content-core's "auto" chain, logging a WARNING that names the engine and the env var that would enable it. Engines with no opt-in runtime (auto/simple/firecrawl/jina) are passed through untouched. The availability probes moved from api/routers/capabilities.py to open_notebook/utils/runtime_capabilities.py so the graph can use them without importing from the API layer; the capabilities endpoint keeps identical behavior and its tests follow the probes to their new home. Found by the smoke-e2e agent during v1.14.0 release testing, on a dev environment that was in exactly this state. Pre-existing since v1.13.0 (#1122 made the runtimes opt-in, #432 made the stored selection take effect), not a v1.14.0 regression. * docs(changelog): record the unavailable-engine fallback fix
128 lines
4.8 KiB
Python
128 lines
4.8 KiB
Python
"""
|
|
Tests for GET /api/capabilities (api/routers/capabilities.py).
|
|
|
|
The endpoint reports the *actual* availability of the opt-in heavy extraction
|
|
runtimes (Docling, Crawl4AI local) so the frontend can gate engine options.
|
|
These tests lock the composition rule: crawl4ai_available is true when EITHER a
|
|
local package is installed OR a remote server is configured.
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
from api.main import app
|
|
|
|
return TestClient(app)
|
|
|
|
|
|
def _patch_probes(monkeypatch, *, docling, crawl4ai_local, crawl4ai_remote):
|
|
monkeypatch.setattr(
|
|
"api.routers.capabilities.docling_available", lambda: docling
|
|
)
|
|
monkeypatch.setattr(
|
|
"api.routers.capabilities.crawl4ai_remote_configured",
|
|
lambda: crawl4ai_remote,
|
|
)
|
|
# Local readiness means package installed AND a Chromium browser present.
|
|
monkeypatch.setattr(
|
|
"api.routers.capabilities.crawl4ai_local_ready", lambda: crawl4ai_local
|
|
)
|
|
|
|
|
|
class TestCapabilitiesEndpoint:
|
|
def test_all_unavailable(self, client, monkeypatch):
|
|
_patch_probes(
|
|
monkeypatch, docling=False, crawl4ai_local=False, crawl4ai_remote=False
|
|
)
|
|
response = client.get("/api/capabilities")
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"docling_available": False,
|
|
"crawl4ai_available": False,
|
|
"crawl4ai_remote_configured": False,
|
|
}
|
|
|
|
def test_docling_available_is_independent_of_crawl4ai(self, client, monkeypatch):
|
|
_patch_probes(
|
|
monkeypatch, docling=True, crawl4ai_local=False, crawl4ai_remote=False
|
|
)
|
|
body = client.get("/api/capabilities").json()
|
|
assert body["docling_available"] is True
|
|
assert body["crawl4ai_available"] is False
|
|
|
|
def test_local_crawl4ai_makes_it_available(self, client, monkeypatch):
|
|
_patch_probes(
|
|
monkeypatch, docling=False, crawl4ai_local=True, crawl4ai_remote=False
|
|
)
|
|
body = client.get("/api/capabilities").json()
|
|
assert body["crawl4ai_available"] is True
|
|
assert body["crawl4ai_remote_configured"] is False
|
|
|
|
def test_remote_crawl4ai_makes_it_available_without_local(
|
|
self, client, monkeypatch
|
|
):
|
|
_patch_probes(
|
|
monkeypatch, docling=False, crawl4ai_local=False, crawl4ai_remote=True
|
|
)
|
|
body = client.get("/api/capabilities").json()
|
|
assert body["crawl4ai_available"] is True
|
|
assert body["crawl4ai_remote_configured"] is True
|
|
|
|
|
|
class TestCrawl4aiLocalReadiness:
|
|
"""Local Crawl4AI needs the package AND a Chromium browser on disk."""
|
|
|
|
def test_not_ready_when_package_missing(self, monkeypatch):
|
|
import open_notebook.utils.runtime_capabilities as cap
|
|
|
|
monkeypatch.setattr(
|
|
cap.importlib.util, "find_spec", lambda name, *a, **k: None
|
|
)
|
|
assert cap.crawl4ai_local_ready() is False
|
|
|
|
def test_not_ready_when_browser_missing(self, monkeypatch, tmp_path):
|
|
import open_notebook.utils.runtime_capabilities as cap
|
|
|
|
monkeypatch.setattr(
|
|
cap.importlib.util, "find_spec", lambda name, *a, **k: object()
|
|
)
|
|
# PLAYWRIGHT_BROWSERS_PATH set to an empty dir → no chromium installed.
|
|
monkeypatch.setenv("PLAYWRIGHT_BROWSERS_PATH", str(tmp_path))
|
|
assert cap.crawl4ai_local_ready() is False
|
|
|
|
def test_ready_when_browser_present(self, monkeypatch, tmp_path):
|
|
import open_notebook.utils.runtime_capabilities as cap
|
|
|
|
monkeypatch.setattr(
|
|
cap.importlib.util, "find_spec", lambda name, *a, **k: object()
|
|
)
|
|
(tmp_path / "chromium-1140").mkdir()
|
|
monkeypatch.setenv("PLAYWRIGHT_BROWSERS_PATH", str(tmp_path))
|
|
assert cap.crawl4ai_local_ready() is True
|
|
|
|
def test_dev_default_cache_without_browser_is_not_ready(
|
|
self, monkeypatch, tmp_path
|
|
):
|
|
"""No PLAYWRIGHT_BROWSERS_PATH: fall back to the default cache, fail closed if empty."""
|
|
import open_notebook.utils.runtime_capabilities as cap
|
|
|
|
monkeypatch.setattr(
|
|
cap.importlib.util, "find_spec", lambda name, *a, **k: object()
|
|
)
|
|
monkeypatch.delenv("PLAYWRIGHT_BROWSERS_PATH", raising=False)
|
|
monkeypatch.setattr(cap, "_default_playwright_cache", lambda: str(tmp_path))
|
|
assert cap.crawl4ai_local_ready() is False
|
|
|
|
def test_dev_default_cache_with_browser_is_ready(self, monkeypatch, tmp_path):
|
|
import open_notebook.utils.runtime_capabilities as cap
|
|
|
|
monkeypatch.setattr(
|
|
cap.importlib.util, "find_spec", lambda name, *a, **k: object()
|
|
)
|
|
monkeypatch.delenv("PLAYWRIGHT_BROWSERS_PATH", raising=False)
|
|
(tmp_path / "chromium-1140").mkdir()
|
|
monkeypatch.setattr(cap, "_default_playwright_cache", lambda: str(tmp_path))
|
|
assert cap.crawl4ai_local_ready() is True
|