deer-flow/backend/tests/test_extension_documentation.py
Wenchao An 6fe87b65fa
feat(extensions): show package-owned usage in extension details (#8031)
* feat(extensions): show package-owned usage in extension details

* test(extensions): align bookmark checks with detail UI

* fix(extensions): reconcile package documentation and locale fallbacks

* fix(extensions): align discovery locale fallback

* fix(extensions): isolate malformed optional documentation

* docs: keep backend guidance within inherited budget
2026-11-10 18:41:27 +08:00

225 lines
9.6 KiB
Python

"""Package-owned copy survives backend-only discovery without importing disabled code."""
import copy
import json
import runpy
from dataclasses import replace
from pathlib import Path
from types import SimpleNamespace
import pytest
from deerflow_extension_api.auth import EXTENSION_PRINCIPAL_RESOLVER_KEY, ExtensionPrincipal
from deerflow_extension_api.plugins import PluginContribution
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.gateway.routers.plugins import router
from deerflow.extensions.documentation import FILENAME, validate_documentation
from deerflow.extensions.loader import ExtensionSpec, load_extensions
from extension_test_fixtures import demo_extensions
SOURCE = "extension_test_fixtures.demo_extensions:install_ok"
PAGE = {
"schema_version": 1,
"id": "example.background",
"locales": {"en-US": {"title": "Background helper", "description": "Keeps tool statistics.", "usage": [{"title": "Run a task", "description": "Statistics are collected automatically."}]}},
}
@pytest.fixture
def package(tmp_path, monkeypatch):
monkeypatch.setattr(demo_extensions, "__file__", str(tmp_path / "__init__.py"))
path = tmp_path / FILENAME
path.write_text(json.dumps(PAGE))
return path
def test_requirements_are_optional_and_empty_requirements_are_omitted():
assert "requirements" not in validate_documentation(PAGE)["locales"]["en-US"]
value = copy.deepcopy(PAGE)
value["locales"]["en-US"]["requirements"] = []
assert "requirements" not in validate_documentation(value)["locales"]["en-US"]
@pytest.mark.parametrize("field", ["title", "description", "usage"])
def test_new_documentation_requires_meaningful_copy(field):
value = copy.deepcopy(PAGE)
del value["locales"]["en-US"][field]
with pytest.raises(ValueError):
validate_documentation(value)
@pytest.mark.parametrize("url", ["javascript:alert(1)", "http://example.org", "https://secret@example.org", "https://example.org:bad/docs", "https://example.org:65536/docs", "https://["])
def test_documentation_links_are_public_https(url):
with pytest.raises(ValueError):
validate_documentation({**PAGE, "docs_url": url})
def test_loader_projects_only_public_documentation(package):
value = copy.deepcopy(PAGE)
value["private_config"] = {"key": "synthetic-secret"}
package.write_text(json.dumps(value))
loaded, diagnostics = load_extensions([ExtensionSpec(use=SOURCE, config={"token": "private"})])
assert diagnostics == []
assert loaded.documentation == ((SOURCE, PAGE),)
# Reads use the loaded snapshot, not mutable files or config on request threads.
package.unlink()
app = FastAPI()
app.state.extensions = loaded
setattr(app.state, EXTENSION_PRINCIPAL_RESOLVER_KEY, lambda request: ExtensionPrincipal("viewer"))
app.include_router(router)
with TestClient(app) as client:
response = client.get("/api/plugins")
assert response.status_code == 200
assert response.headers["cache-control"] == "private, no-store"
(entry,) = response.json()
assert entry["namespace"] == PAGE["id"]
assert entry["documentation"] == PAGE
assert entry["runtime_only"] is True
assert entry["module"] is None
assert entry["settings"] == {"enabled": True}
assert "private" not in response.text
@pytest.mark.parametrize(
"tags,expected",
[
(["zh-CN", "en"], "English"),
(["zh-CN", "en-us"], "English"),
(["zh-CN", "en-GB"], "English"),
(["zh-CN", "en", "en-us"], "US English"),
(["zh-CN", "fr"], "中文"),
],
)
def test_backend_only_discovery_prefers_english_language_variants(package, tags, expected):
page = copy.deepcopy(PAGE)
page["locales"] = {}
titles = {"zh-CN": "中文", "fr": "Français"}
if len(tags) == 3:
titles["en-us"] = "US English"
for tag in tags:
localized = copy.deepcopy(PAGE["locales"]["en-US"])
localized["title"] = titles.get(tag, "English")
page["locales"][tag] = localized
package.write_text(json.dumps(page))
loaded, diagnostics = load_extensions([ExtensionSpec(use=SOURCE)])
assert not diagnostics
app = FastAPI()
app.state.extensions = loaded
setattr(app.state, EXTENSION_PRINCIPAL_RESOLVER_KEY, lambda request: ExtensionPrincipal("viewer"))
app.include_router(router)
with TestClient(app) as client:
response = client.get("/api/plugins")
assert response.status_code == 200
(entry,) = response.json()
assert entry["title"] == expected
assert entry["documentation"] == page
def test_bad_documentation_does_not_disable_working_extension(package):
package.write_text('{"secret":"synthetic-secret"}')
loaded, diagnostics = load_extensions([ExtensionSpec(use=SOURCE)])
assert loaded.has_middleware_contributors
assert loaded.documentation == ()
assert [(item.level, item.source, item.message) for item in diagnostics] == [("warning", SOURCE, "Extension documentation is invalid.")]
@pytest.mark.parametrize("field", ["id", "locale", "title", "description", "usage_title", "usage_description", "requirements", "docs_url"])
@pytest.mark.parametrize("surrogate", ["\ud800", "\udfff"])
def test_unencodable_public_documentation_cannot_break_discovery(package, field, surrogate):
page = copy.deepcopy(PAGE)
localized = page["locales"]["en-US"]
if field == "id":
page["id"] = surrogate
elif field == "locale":
page["locales"] = {surrogate: localized}
elif field.startswith("usage_"):
localized["usage"][0][field.removeprefix("usage_")] = surrogate
elif field == "requirements":
localized["requirements"] = [surrogate]
elif field == "docs_url":
page["docs_url"] = "https://example.org/" + surrogate
else:
localized[field] = surrogate
package.write_text(json.dumps(page))
loaded, diagnostics = load_extensions([ExtensionSpec(use=SOURCE)])
assert loaded.has_middleware_contributors
healthy = PluginContribution(namespace="community.bookmarks", title="Bookmarks", enabled=True)
app = FastAPI()
app.state.extensions = replace(loaded, plugins=(("healthy:install", healthy),))
setattr(app.state, EXTENSION_PRINCIPAL_RESOLVER_KEY, lambda request: ExtensionPrincipal("viewer"))
app.include_router(router)
with TestClient(app) as client:
response = client.get("/api/plugins")
assert response.status_code == 200
assert [entry["namespace"] for entry in response.json()] == [healthy.namespace]
assert loaded.documentation == ()
assert [(item.level, item.source, item.message) for item in diagnostics] == [("warning", SOURCE, "Extension documentation is invalid.")]
def test_unicode_documentation_and_ignored_private_fields_remain_valid():
page = copy.deepcopy(PAGE)
page["locales"]["en-US"]["title"] = "指南 🦌"
page["docs_url"] = "https://example.org:8443/docs"
expected = copy.deepcopy(page)
page["private_config"] = {"ignored": "\ud800"}
assert validate_documentation(page) == expected
def test_missing_documentation_keeps_legacy_behavior(package):
package.unlink()
loaded, diagnostics = load_extensions([ExtensionSpec(use=SOURCE)])
assert loaded.has_middleware_contributors
assert not loaded.documentation and not diagnostics
def test_disabled_or_failed_install_cannot_publish_documentation(package):
for spec in [ExtensionSpec(use=SOURCE, enabled=False), ExtensionSpec(use=SOURCE.replace("install_ok", "install_partial_then_raise"))]:
loaded, _ = load_extensions([spec])
assert not loaded.documentation
assert not loaded.has_middleware_contributors
def test_bundled_documentation_snapshot_is_current():
root = Path(__file__).resolve().parents[2]
script = runpy.run_path(str(root / "scripts/sync_extension_documentation.py"))
assert script["DESTINATION"].read_text(encoding="utf-8") == script["render"]()
assert len(json.loads(script["render"]())) == 15
@pytest.mark.parametrize("collision", [False, True])
def test_plugin_discovery_fences_documentation_identity(collision):
plugin = PluginContribution(namespace="example.background", title="Plugin", description="Plugin copy")
page = copy.deepcopy(PAGE)
page["id"] = plugin.namespace if collision else "another.plugin"
source = "backend.only:install" if collision else SOURCE
app = FastAPI()
app.state.extensions = SimpleNamespace(plugins=((SOURCE, plugin),), documentation=((source, page),))
setattr(app.state, EXTENSION_PRINCIPAL_RESOLVER_KEY, lambda request: ExtensionPrincipal("viewer"))
app.include_router(router)
with TestClient(app) as client:
response = client.get("/api/plugins")
assert response.status_code == 200
(entry,) = response.json()
assert entry["namespace"] == plugin.namespace
assert entry["documentation"] is None
def test_bundled_generator_rejects_missing_language_and_shared_size_limit(tmp_path, monkeypatch):
root = Path(__file__).resolve().parents[2]
script = runpy.run_path(str(root / "scripts/sync_extension_documentation.py"))
namespace = script["render"].__globals__
monkeypatch.setitem(namespace, "ROOT", tmp_path)
path = tmp_path / "examples/deerflow-extension-test/package/deerflow-extension.json"
path.parent.mkdir(parents=True)
path.write_text(json.dumps(PAGE))
with pytest.raises(ValueError, match="English and Chinese"):
script["render"]()
value = copy.deepcopy(PAGE)
value["locales"]["zh"] = copy.deepcopy(value["locales"]["en-US"])
path.write_text(json.dumps(value))
assert "deerflow-extension-test" in script["render"]()
monkeypatch.setitem(namespace, "MAX_BYTES", 1)
with pytest.raises(ValueError, match="too large"):
script["render"]()