open-notebook/tests/test_capabilities_api.py
Luis Novo 906ad1698a
feat(sources): opt-in Docling + Crawl4AI runtimes installed at startup (#1122) (#1123)
Docling and local Crawl4AI are now opt-in, installed on first container startup
via OPEN_NOTEBOOK_ENABLE_DOCLING / OPEN_NOTEBOOK_ENABLE_CRAWL4AI, keeping the
default image lean (Crawl4AI un-bundled). Downloads cache on the /app/data
volume; installs are blocking with loud logs and degrade-don't-die on failure.
A GET /api/capabilities probe reports actual availability and the Settings UI
gates the Docling/Crawl4AI engines and OCR toggle (with env-var hints) until the
runtime is present. Docs folded from #1121 and rewritten to opt-in; ADR-007 added.

Closes #1122. Closes #1105.
2026-07-13 08:26:20 -03:00

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 api.routers.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 api.routers.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 api.routers.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 api.routers.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 api.routers.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