deer-flow/backend/tests/test_knowledge_platform_extension.py
Rei 9cb2bde557
test(ci): run bundled extension behavior suites outside the core shards (#8180)
Bundled examples keep their behavior tests in backend/tests/, so the core
backend workflow ran optional extension behavior on every pull request. Mark
those suites with pytest.mark.bundled_extension, exclude them from the core
shards and from the duration baseline, and run them in a dedicated workflow
through make test-bundled-extensions. Tests of host mechanisms stay in the
core shards even when an example package is the payload.

The marked suites import deerflow.agents, config, runtime, knowledge and
app.gateway, so the workflow triggers for backend/packages/**,
backend/app/gateway/** and the marked test modules; a backend test pins those
paths, the make target and both marker selections, and fails when a marked
module is missing from the path list. Host/extension compatibility checks — a
host API gate and authorization of contributed routes — live in
test_extension_host_contracts.py so they still fail core CI. The committed
duration baseline drops the 482 entries for the excluded suites.

Refs #8173

Co-authored-by: REI-330 <173389910+REI-330@users.noreply.github.com>
Co-authored-by: willem-bd <143703838+willem-bd@users.noreply.github.com>
Co-authored-by: Eilen6316 <136898293+Eilen6316@users.noreply.github.com>
2026-11-14 09:12:25 +08:00

743 lines
34 KiB
Python

"""Synthetic platform policy, persistence and management composition regressions."""
import asyncio
from pathlib import Path
import pytest
from deerflow_extension_api.auth import ExtensionPrincipal
pytestmark = pytest.mark.bundled_extension
@pytest.fixture
def platform(tmp_path, monkeypatch):
root = Path(__file__).resolve().parents[2]
for package in ("knowledge-platform", "knowledge-management"):
monkeypatch.syspath_prepend(str(root / "examples" / f"deerflow-extension-{package}"))
from deerflow_extension_knowledge_platform.models import Settings, resource_key
from deerflow_extension_knowledge_platform.store import Store
resource = resource_key("weknora", "local", "kb")
settings = Settings.model_validate(
{"enabled": True, "storage_path": str(tmp_path / "platform.db"), "connections": [{"id": "local", "provider": "weknora", "provider_config": {}, "resources": [{"base_id": "kb", "dataset_id": "wk1_fixture"}]}]}
)
store = Store(settings)
return store, settings, resource
ADMIN = ExtensionPrincipal("alice", is_admin=True)
MEMBER = ExtensionPrincipal("bob")
def document(resource):
return {
"connections": {"local": {}},
"spaces": {
"org": {"title": "Organization", "resources": [resource], "members": {"alice": "manager", "bob": "viewer"}},
"division": {"title": "Division", "parent_id": "org", "processing": {"summary_enabled": True}},
"team": {"title": "Team", "parent_id": "division", "operations": ["capabilities", "status", "manual"], "processing": {"summary_enabled": False}},
"project": {"title": "Project", "parent_id": "team"},
},
}
def test_four_level_inheritance_and_restart(platform):
from deerflow_extension_knowledge_platform.store import Store
store, settings, resource = platform
result = store.replace(ADMIN, document(resource), 0)
assert result["revision"] == 1
effective = Store(settings).effective(ADMIN, "project")
assert effective["resources"][0]["dataset_id"] == "wk1_fixture"
assert effective["resources"][0]["processing"] == {"summary_enabled": False}
assert set(effective["resources"][0]["operations"]) == {"capabilities", "status", "manual"}
assert effective["role"] == "manager"
def deep_policy(settings, directory, resources_per_connection=40, connections=5):
from deerflow_extension_knowledge_platform.models import Settings, resource_key
from deerflow_extension_knowledge_platform.store import Store
config = settings.model_dump()
config["storage_path"] = str(directory / "deep.db")
config["connections"] = [{"id": f"c{i}", "provider": "weknora", "resources": [{"base_id": f"b{j}", "dataset_id": f"wk1_{i}_{j}"} for j in range(resources_per_connection)]} for i in range(connections)]
store = Store(Settings.model_validate(config))
spaces = {f"s{i}": {"title": f"Space {i}", "parent_id": f"s{i - 1}"} for i in range(1, 500)}
spaces["s0"] = {"title": "Root", "resources": list(store.resources), "members": {"alice": "manager", "bob": "viewer"}}
spaces["s250"]["processing"] = {"chunk_size": 256, "chunk_overlap": 32}
spaces["s499"].update(processing={"summary_enabled": False}, default_tool=None, tools=[])
# Resolve leaves before parents to exercise arbitrary policy JSON order.
policy = {
"defaults": {"default_tool": "knowledge_search", "processing": {"summary_enabled": True}},
"connections": {f"c{i}": {"processing": {"graph_enabled": bool(i % 2)}, "default_tool": "knowledge_entries_search" if i == 4 else "knowledge_search"} for i in range(connections)},
"spaces": dict(reversed(list(spaces.items()))),
}
return store, policy, resource_key("weknora", "c4", "b0")
@pytest.mark.parametrize(("connections", "resources_per_connection"), [(5, 1), (5, 40), (20, 200)])
def test_deep_policy_does_not_reserialize_processing_per_resource_ancestor(platform, tmp_path, monkeypatch, connections, resources_per_connection):
from deerflow_extension_knowledge_management.contracts import ProcessingOptions
_, settings, _ = platform
store, policy, resource = deep_policy(settings, tmp_path, resources_per_connection, connections)
original = ProcessingOptions.model_dump
dumps = 0
def counted_dump(self, *args, **kwargs):
nonlocal dumps
dumps += 1
# A depth/resource product used to perform 25M dumps for this valid tree.
assert dumps <= 2000, "Processing serialization multiplied by resource count and ancestor depth"
return original(self, *args, **kwargs)
monkeypatch.setattr(ProcessingOptions, "model_dump", counted_dump)
store.replace(ADMIN, policy, 0)
effective = store.effective(MEMBER, "s499")
assert effective["role"] == "viewer"
assert len(effective["resources"]) == resources_per_connection * connections
item = next(item for item in effective["resources"] if item["resource_id"] == resource)
assert item["processing"] == {"summary_enabled": False, "graph_enabled": False, "chunk_size": 256, "chunk_overlap": 32}
assert item["default_tool"] is None and item["tools"] == []
assert item["operations"] == ["capabilities", "status"]
inherited = next(item for item in store.effective(MEMBER, "s498")["resources"] if item["resource_id"] == resource)
assert inherited["default_tool"] == "knowledge_entries_search"
def test_listing_deep_spaces_does_not_build_processing_or_resource_projections(platform, tmp_path, monkeypatch):
from deerflow_extension_knowledge_management.contracts import ProcessingOptions
_, settings, _ = platform
store, policy, _ = deep_policy(settings, tmp_path, resources_per_connection=1)
store.replace(ADMIN, policy, 0)
def unexpected(*args, **kwargs):
pytest.fail("Space listing materialized management processing/resource projections")
monkeypatch.setattr(store, "_effective", unexpected)
monkeypatch.setattr(ProcessingOptions, "model_dump", unexpected)
result = store.spaces(MEMBER)
assert result["revision"] == 1
assert len(result["items"]) == 500
assert all(set(item) == {"space_id", "title", "role"} and item["role"] == "viewer" for item in result["items"])
@pytest.mark.parametrize("change", ["resource_removed", "tool_revoked", "connection_disabled", "ancestor_disabled", "member_blocked", "ancestor_revoked"])
def test_listing_keeps_detail_admission_after_policy_or_operator_changes(platform, change):
from deerflow_extension_knowledge_platform.models import Settings
from deerflow_extension_knowledge_platform.store import Store
store, settings, resource = platform
data = document(resource)
data["defaults"] = {"default_tool": "knowledge_search"}
data["spaces"]["unrelated"] = {"title": "Unrelated", "resources": [], "members": {"bob": "viewer"}}
expected = {"org", "division", "team", "project", "unrelated"}
if change == "connection_disabled":
data["connections"]["local"]["enabled"] = False
elif change == "ancestor_disabled":
data["spaces"]["division"]["enabled"] = False
expected -= {"division", "team", "project"}
elif change == "member_blocked":
data["spaces"]["team"]["members"] = {"bob": "blocked"}
expected -= {"team", "project"}
elif change == "ancestor_revoked":
del data["spaces"]["org"]["members"]["bob"]
expected = {"unrelated"}
store.replace(ADMIN, data, 0)
changed = settings.model_dump()
if change == "resource_removed":
changed["connections"][0]["resources"] = [{"base_id": "replacement", "dataset_id": "wk1_replacement"}]
expected = {"unrelated"}
elif change == "tool_revoked":
changed["connections"][0]["resources"][0]["tools"] = []
expected = {"unrelated"}
store = Store(Settings.model_validate(changed))
assert {item["space_id"] for item in store.spaces(MEMBER)["items"]} == expected
for space_id in data["spaces"]:
if space_id in expected:
assert store.effective(MEMBER, space_id)["role"] == "viewer"
else:
with pytest.raises(PermissionError):
store.effective(MEMBER, space_id)
def test_blocked_members_cannot_be_unblocked_below_or_leak_into_siblings(platform):
store, _, resource = platform
data = document(resource)
data["spaces"]["team"]["members"] = {"bob": "blocked"}
data["spaces"]["sibling"] = {"title": "Sibling", "parent_id": "division"}
store.replace(ADMIN, data, 0)
assert {item["space_id"] for item in store.spaces(MEMBER)["items"]} == {"org", "division", "sibling"}
data["spaces"]["project"]["members"] = {"bob": "viewer"}
with pytest.raises(ValueError):
store.replace(ADMIN, data, 1)
assert store.read(ADMIN)["revision"] == 1
def test_members_cannot_edit_or_escalate(platform):
store, _, resource = platform
store.replace(ADMIN, document(resource), 0)
with pytest.raises(PermissionError):
store.replace(MEMBER, document(resource), 1)
effective = store.effective(MEMBER, "project")
assert set(effective["resources"][0]["operations"]) == {"capabilities", "status"}
with pytest.raises(PermissionError):
store.effective(ExtensionPrincipal("outsider", is_admin=True), "project")
@pytest.mark.parametrize("change", ["cycle", "missing_parent", "resource_widening", "operation_widening", "role_widening", "unknown_connection"])
def test_invalid_graph_rolls_back(platform, change):
store, _, resource = platform
data = document(resource)
store.replace(ADMIN, data, 0)
if change == "cycle":
data["spaces"]["org"]["parent_id"] = "project"
elif change == "missing_parent":
data["spaces"]["team"]["parent_id"] = "missing"
elif change == "resource_widening":
data["spaces"]["team"]["resources"] = ["unmapped"]
elif change == "operation_widening":
data["spaces"]["project"]["operations"] = ["upload"]
elif change == "role_widening":
data["spaces"]["project"]["members"] = {"bob": "manager"}
else:
data["connections"]["evil"] = {}
with pytest.raises(ValueError):
store.replace(ADMIN, data, 1)
assert store.read(ADMIN)["revision"] == 1
def test_ancestor_revocation_and_connection_disable(platform):
store, _, resource = platform
data = document(resource)
store.replace(ADMIN, data, 0)
del data["spaces"]["org"]["members"]["bob"]
store.replace(ADMIN, data, 1)
with pytest.raises(PermissionError):
store.effective(MEMBER, "project")
data["connections"]["local"]["enabled"] = False
store.replace(ADMIN, data, 2)
assert store.effective(ADMIN, "project")["resources"] == []
def test_sharing_is_explicit_and_same_named_resources_do_not_alias(platform):
from deerflow_extension_knowledge_platform.models import resource_key
store, _, resource = platform
assert resource != resource_key("weknora", "other", "kb")
data = document(resource)
data["spaces"]["other"] = {"title": "Other organization", "resources": [], "members": {"bob": "viewer"}}
store.replace(ADMIN, data, 0)
assert store.effective(MEMBER, "other")["resources"] == []
data["spaces"]["other"]["resources"] = [resource]
store.replace(ADMIN, data, 1)
assert store.effective(MEMBER, "other")["resources"][0]["resource_id"] == resource
def test_concurrent_writers_conflict_instead_of_losing_update(platform):
from concurrent.futures import ThreadPoolExecutor
from deerflow_extension_knowledge_platform.store import RevisionConflict
store, _, resource = platform
def write():
try:
store.replace(ADMIN, document(resource), 0)
return "saved"
except RevisionConflict:
return "conflict"
with ThreadPoolExecutor(max_workers=2) as pool:
assert sorted(pool.map(lambda _: write(), range(2))) == ["conflict", "saved"]
def _process_writer(settings_data, resource):
from deerflow_extension_knowledge_platform.models import Settings
from deerflow_extension_knowledge_platform.store import RevisionConflict, Store
try:
Store(Settings.model_validate(settings_data)).replace(ADMIN, document(resource), 0)
return "saved"
except RevisionConflict:
return "conflict"
def test_independent_processes_share_the_sqlite_revision_fence(platform):
import multiprocessing
from concurrent.futures import ProcessPoolExecutor
_, settings, resource = platform
with ProcessPoolExecutor(max_workers=2, mp_context=multiprocessing.get_context("spawn")) as pool:
futures = [pool.submit(_process_writer, settings.model_dump(), resource) for _ in range(2)]
assert sorted(future.result(timeout=30) for future in futures) == ["conflict", "saved"]
@pytest.mark.asyncio
async def test_space_management_preserves_principal_and_applies_defaults(platform):
from deerflow_extension_knowledge_management.contracts import Capabilities, IngestRequest, ProcessingResult
from deerflow_extension_knowledge_platform.service import Platform
store, settings, resource = platform
data = document(resource)
store.replace(ADMIN, data, 0)
calls = []
class Provider:
async def capabilities(self, context, connection, base):
return Capabilities(operations={"capabilities", "manual", "status"}, processing_options={"summary_enabled"}, pipeline={})
async def ingest(self, context, connection, base, body):
calls.append((context.principal, connection, base, body.processing))
return ProcessingResult(provider="weknora", connection_id="local", dataset_id="wk1_fixture", document_id="doc", state="queued", provider_state="pending", searchable=False)
service = Platform(settings, store, {"local": Provider()})
body = IngestRequest(kind="manual", title="Synthetic", content="Cedar")
await service.invoke(ADMIN, "project", resource, "manual", body)
assert calls[0][:3] == (ADMIN, "local", "kb")
assert calls[0][3].summary_enabled is False
for principal, operation in [(MEMBER, "manual"), (ExtensionPrincipal("alice", is_admin=True, is_internal=True), "manual"), (ADMIN, "upload")]:
with pytest.raises(PermissionError):
await service.invoke(principal, "project", resource, operation, body)
assert len(calls) == 1
@pytest.mark.asyncio
async def test_policy_denial_before_any_provider_call(platform):
from deerflow_extension_knowledge_platform.service import Platform
store, settings, resource = platform
store.replace(ADMIN, document(resource), 0)
class Provider:
def __getattr__(self, name):
pytest.fail("Denied request contacted provider")
service = Platform(settings, store, {"local": Provider()})
with pytest.raises(PermissionError):
await service.invoke(ADMIN, "project", "unknown-resource", "status", "doc")
@pytest.mark.asyncio
async def test_http_trusted_auth_and_bounded_input(platform):
import httpx
from deerflow_extension_knowledge_platform.router import build_router
from deerflow_extension_knowledge_platform.service import Platform
from fastapi import FastAPI
store, settings, resource = platform
app = FastAPI()
app.include_router(build_router(Platform(settings, store, {})))
principal = ADMIN
scopes = []
app.state.deerflow_extension_principal_resolver = lambda request: principal
async def authorize(request, namespace, scope):
scopes.append((namespace, scope))
return True
app.state.deerflow_extension_plugin_authz_resolver_async = authorize
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
response = await client.put("/api/extensions/knowledge-platform/policy", json={"revision": 0, "policy": document(resource)})
assert response.status_code == 200
response = await client.put("/api/extensions/knowledge-platform/policy", json={"revision": 0, "policy": document(resource)})
assert response.status_code == 409
principal = MEMBER
assert (await client.get("/api/extensions/knowledge-platform/spaces/project")).status_code == 200
assert (await client.get("/api/extensions/knowledge-platform/policy")).status_code == 403
assert (await client.post(f"/api/extensions/knowledge-platform/spaces/project/resources/{resource}/documents", json={"user_id": "alice"})).status_code == 403
principal = ADMIN
assert (await client.put("/api/extensions/knowledge-platform/policy", content=b"x" * (1024 * 1024 + 1))).status_code == 413
assert (await client.put("/api/extensions/knowledge-platform/policy", json={"revision": 1, "policy": document(resource), "user_id": "alice"})).status_code == 422
principal = None
assert (await client.get("/api/extensions/knowledge-platform/spaces")).status_code == 403
assert ("knowledge-platform", "write") in scopes
@pytest.mark.asyncio
@pytest.mark.parametrize("method", ["GET", "PUT"])
async def test_policy_storage_lock_returns_503_without_mutating(platform, monkeypatch, method):
import sqlite3
from contextlib import closing
import httpx
from deerflow_extension_knowledge_platform.router import build_router
from deerflow_extension_knowledge_platform.service import Platform
from fastapi import FastAPI
store, settings, resource = platform
app = FastAPI()
app.include_router(build_router(Platform(settings, store, {})))
app.state.deerflow_extension_principal_resolver = lambda request: ADMIN
async def authorize(*args):
return True
app.state.deerflow_extension_plugin_authz_resolver_async = authorize
# Exercise the real busy-timeout error without waiting the production ten seconds.
monkeypatch.setattr(store, "connect", lambda: sqlite3.connect(store.path, timeout=0.01))
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, raise_app_exceptions=False), base_url="http://test") as client:
with closing(sqlite3.connect(store.path)) as owner:
owner.execute("BEGIN EXCLUSIVE")
response = await client.request(method, "/api/extensions/knowledge-platform/policy", **({"json": {"revision": 0, "policy": document(resource)}} if method == "PUT" else {}))
assert response.status_code == 503
assert response.json() == {"detail": "Policy storage is busy or unavailable; reload policy before retrying"}
assert str(store.path) not in response.text
snapshot = await client.get("/api/extensions/knowledge-platform/policy")
assert snapshot.status_code == 200 and snapshot.json()["revision"] == 0
saved = await client.put("/api/extensions/knowledge-platform/policy", json={"revision": 0, "policy": document(resource)})
assert saved.status_code == 200 and saved.json()["revision"] == 1
@pytest.mark.asyncio
async def test_unavailable_policy_database_returns_sanitized_503(platform):
import httpx
from deerflow_extension_knowledge_platform.router import build_router
from deerflow_extension_knowledge_platform.service import Platform
from fastapi import FastAPI
store, settings, _ = platform
store.path.write_bytes(b"invalid SQLite database with private diagnostic content")
app = FastAPI()
app.include_router(build_router(Platform(settings, store, {})))
app.state.deerflow_extension_principal_resolver = lambda request: ADMIN
async def authorize(*args):
return True
app.state.deerflow_extension_plugin_authz_resolver_async = authorize
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, raise_app_exceptions=False), base_url="http://test") as client:
response = await client.get("/api/extensions/knowledge-platform/policy")
assert response.status_code == 503
assert response.json() == {"detail": "Policy storage is busy or unavailable; reload policy before retrying"}
assert "private diagnostic" not in response.text and str(store.path) not in response.text
@pytest.mark.asyncio
async def test_platform_database_work_runs_off_loop(platform, monkeypatch):
from deerflow_extension_knowledge_platform.service import Platform
store, settings, _ = platform
original = store.read
def read(principal):
with pytest.raises(RuntimeError):
asyncio.get_running_loop()
return original(principal)
monkeypatch.setattr(store, "read", read)
assert (await Platform(settings, store, {}).policy(ADMIN))["revision"] == 0
@pytest.mark.asyncio
async def test_revocation_during_capability_lookup_prevents_write(platform):
from deerflow_extension_knowledge_management.contracts import Capabilities, IngestRequest
from deerflow_extension_knowledge_platform.service import Platform
store, settings, resource = platform
data = document(resource)
store.replace(ADMIN, data, 0)
class Provider:
async def capabilities(self, *args):
data["connections"]["local"]["enabled"] = False
await asyncio.to_thread(store.replace, ADMIN, data, 1)
return Capabilities(operations={"manual"}, processing_options={"summary_enabled"}, pipeline={})
async def ingest(self, *args):
pytest.fail("Revoked policy submitted an upstream write")
with pytest.raises(PermissionError):
await Platform(settings, store, {"local": Provider()}).invoke(ADMIN, "project", resource, "manual", IngestRequest(kind="manual", title="Test", content="Test"))
@pytest.mark.asyncio
async def test_real_management_adapter_composition(platform, monkeypatch):
import hashlib
import json
import httpx
from deerflow_extension_knowledge_management.contracts import IngestRequest
from deerflow_extension_knowledge_management.weknora import WeKnoraManagement
from deerflow_extension_knowledge_platform.service import Platform
store, settings, resource = platform
dataset = "wk1_" + hashlib.sha256(json.dumps(["https://weknora.invalid", "1", "kb"], ensure_ascii=False).encode()).hexdigest()
updated = settings.model_dump()
updated["connections"][0]["resources"][0]["dataset_id"] = dataset
from deerflow_extension_knowledge_platform.models import Settings
from deerflow_extension_knowledge_platform.store import Store
settings = Settings.model_validate(updated)
store = Store(settings)
store.replace(ADMIN, document(resource), 0)
monkeypatch.setenv("PLATFORM_SYNTHETIC_KEY", "synthetic-secret")
calls, code = [], [200]
def handler(request):
calls.append(request)
assert request.headers["X-API-Key"] == "synthetic-secret"
if code[0] != 200:
return httpx.Response(code[0], text="synthetic-secret")
if request.url.path == "/api/v1/knowledge-bases/kb":
data = {"id": "kb", "capabilities": {"wiki": True}, "chunking_config": {"chunk_size": 1024, "chunk_overlap": 128}}
else:
data = {"id": "doc", "knowledge_base_id": "kb", "parse_status": "pending", "enable_status": "enabled"}
return httpx.Response(200, json={"success": True, "data": data})
provider = WeKnoraManagement(
{
"connections": [
{
"id": "local",
"base_url": "https://weknora.invalid",
"tenant_id": "1",
"api_key_env": "PLATFORM_SYNTHETIC_KEY",
"grants": [{"user_id": "alice", "knowledge_base_ids": ["kb"], "operations": ["capabilities", "manual", "status"]}],
}
]
},
transport=httpx.MockTransport(handler),
)
service = Platform(settings, store, {"local": provider})
body = IngestRequest(kind="manual", title="Synthetic", content="Cedar")
result = await service.invoke(ADMIN, "project", resource, "manual", body)
assert result.dataset_id == dataset and result.state == "queued"
assert json.loads(calls[-1].content)["process_config"]["summary_enabled"] is False
status = await service.invoke(ADMIN, "project", resource, "status", "doc")
assert status.document_id == "doc"
code[0] = 403
before = len(calls)
with pytest.raises(PermissionError):
await service.invoke(ADMIN, "project", resource, "manual", body)
assert len(calls) == before + 1
# Correct space policy still cannot bypass the existing provider's exact-user grant.
data = document(resource)
data["spaces"]["org"]["members"]["unbound"] = "manager"
store.replace(ADMIN, data, 1)
before = len(calls)
with pytest.raises(PermissionError):
await service.invoke(ExtensionPrincipal("unbound", is_admin=True), "project", resource, "manual", body)
assert len(calls) == before
@pytest.mark.asyncio
async def test_mismatched_result_identity_is_not_remapped(platform):
from deerflow_extension_knowledge_management.contracts import ProcessingResult
from deerflow_extension_knowledge_platform.service import Platform
store, settings, resource = platform
store.replace(ADMIN, document(resource), 0)
class Provider:
async def status(self, *args):
return ProcessingResult(provider="weknora", connection_id="other", dataset_id="wrong", document_id="doc", state="completed", provider_state="completed", searchable=True)
with pytest.raises(RuntimeError, match="identity"):
await Platform(settings, store, {"local": Provider()}).invoke(ADMIN, "project", resource, "status", "doc")
def test_connections_and_tools_limit_effective_capabilities(platform):
store, _, resource = platform
data = document(resource)
data["connections"]["local"] = {"operations": ["status"], "tools": ["knowledge_search"], "default_tool": "knowledge_search"}
store.replace(ADMIN, data, 0)
item = store.effective(ADMIN, "project")["resources"][0]
assert item["operations"] == ["status"]
assert item["tools"] == ["knowledge_search"]
data["spaces"]["project"]["default_tool"] = "entity_relations_search"
with pytest.raises(ValueError):
store.replace(ADMIN, data, 1)
def test_platform_defaults_are_inherited_with_a_permission_ceiling(platform):
store, _, resource = platform
data = document(resource)
data["defaults"] = {"operations": ["capabilities", "status", "manual"], "processing": {"graph_enabled": False}, "tools": ["knowledge_search"], "default_tool": "knowledge_search"}
store.replace(ADMIN, data, 0)
item = store.effective(ADMIN, "project")["resources"][0]
assert item["processing"] == {"graph_enabled": False, "summary_enabled": False}
assert item["tools"] == ["knowledge_search"]
assert item["default_tool"] == "knowledge_search"
assert set(item["operations"]) == {"capabilities", "status", "manual"}
def test_child_can_explicitly_clear_inherited_default_tool(platform):
store, _, resource = platform
data = document(resource)
data["defaults"] = {"default_tool": "knowledge_search"}
data["spaces"]["project"].update(tools=[], default_tool=None)
store.replace(ADMIN, data, 0)
item = store.effective(ADMIN, "project")["resources"][0]
assert item["tools"] == [] and item["default_tool"] is None
snapshot = store.read(ADMIN)
store.replace(ADMIN, snapshot["policy"], 1)
assert store.effective(ADMIN, "team")["resources"][0]["default_tool"] == "knowledge_search"
assert store.effective(ADMIN, "project")["resources"][0]["default_tool"] is None
def test_operator_resource_removal_can_be_reconciled_without_global_outage(platform):
from deerflow_extension_knowledge_platform.models import Settings
from deerflow_extension_knowledge_platform.store import Store
store, settings, resource = platform
data = document(resource)
data["spaces"]["unrelated"] = {"title": "Unrelated", "resources": [], "members": {"alice": "manager"}}
store.replace(ADMIN, data, 0)
changed = settings.model_dump()
changed["connections"][0]["resources"] = [{"base_id": "replacement", "dataset_id": "wk1_replacement"}]
store = Store(Settings.model_validate(changed))
snapshot = store.read(ADMIN)
assert snapshot["revision"] == 1
assert store.effective(ADMIN, "unrelated")["resources"] == []
with pytest.raises(PermissionError):
store.effective(ADMIN, "project")
assert [s["space_id"] for s in store.spaces(ADMIN)["items"]] == ["unrelated"]
data["spaces"]["org"]["resources"] = []
assert store.replace(ADMIN, data, 1)["revision"] == 2
def test_operator_tool_revocation_cannot_leave_an_invalid_default(platform):
from deerflow_extension_knowledge_platform.models import Settings
from deerflow_extension_knowledge_platform.store import Store
store, settings, resource = platform
data = document(resource)
data["defaults"] = {"default_tool": "knowledge_search"}
store.replace(ADMIN, data, 0)
changed = settings.model_dump()
changed["connections"][0]["resources"][0]["tools"] = []
store = Store(Settings.model_validate(changed))
assert store.read(ADMIN)["revision"] == 1
with pytest.raises(PermissionError):
store.effective(ADMIN, "project")
def test_management_namespace_can_be_registered_without_direct_routes(platform):
from deerflow_extension_knowledge_management import install
class Registry:
plugins, mounted = [], []
def plugin(self, contribution):
self.plugins.append(contribution)
return True
def routers(self, routes):
self.mounted.extend(routes)
registry = Registry()
install(registry, {"enabled": True, "routes_enabled": False})
assert len(registry.plugins) == 1
assert registry.plugins[0].namespace == "knowledge-management"
assert not registry.mounted
def test_policy_never_serializes_operator_configuration(platform):
import json
store, _, _ = platform
assert "provider_config" not in json.dumps(store.read(ADMIN))
@pytest.mark.asyncio
async def test_dual_namespace_authorization_is_required(platform):
import httpx
from deerflow_extension_knowledge_platform.router import build_router
from deerflow_extension_knowledge_platform.service import Platform
from fastapi import FastAPI
store, settings, resource = platform
store.replace(ADMIN, document(resource), 0)
app = FastAPI()
app.include_router(build_router(Platform(settings, store, {})))
app.state.deerflow_extension_principal_resolver = lambda request: ADMIN
async def authorize(request, namespace, scope):
return namespace == "knowledge-platform"
app.state.deerflow_extension_plugin_authz_resolver_async = authorize
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
response = await client.get(f"/api/extensions/knowledge-platform/spaces/project/resources/{resource}/capabilities")
assert response.status_code == 403
@pytest.mark.asyncio
@pytest.mark.parametrize("operation", ["upload", "reprocess", "cancel"])
async def test_remaining_management_operations_preserve_contract(platform, operation):
from deerflow_extension_knowledge_management.contracts import Capabilities, ProcessingOptions, ProcessingResult
from deerflow_extension_knowledge_platform.service import Platform
store, settings, resource = platform
data = document(resource)
del data["spaces"]["team"]["operations"]
store.replace(ADMIN, data, 0)
calls = []
result = ProcessingResult(provider="weknora", connection_id="local", dataset_id="wk1_fixture", document_id="doc", state="unknown", provider_state="new_upstream_state", searchable=False)
class Provider:
async def capabilities(self, *args):
return Capabilities(operations={"upload", "reprocess", "cancel"}, processing_options={"summary_enabled"}, pipeline={})
async def upload(self, context, connection, base, filename, content, options):
calls.append((filename, content, options.model_dump(exclude_none=True)))
return result
async def process(self, context, connection, base, document, action, options):
calls.append((document, action, options.model_dump(exclude_none=True)))
return result
value = await Platform(settings, store, {"local": Provider()}).invoke(ADMIN, "project", resource, operation, ProcessingOptions(), document="doc", filename="guide.txt", content=b"synthetic")
assert value == result # No fabricated success for an unknown upstream state.
assert calls == ([("guide.txt", b"synthetic", {"summary_enabled": False})] if operation == "upload" else [("doc", operation, {})])
@pytest.mark.asyncio
async def test_unsupported_options_do_not_mutate_or_retry(platform):
from deerflow_extension_knowledge_management.contracts import Capabilities, IngestRequest
from deerflow_extension_knowledge_platform.service import Platform
store, settings, resource = platform
store.replace(ADMIN, document(resource), 0)
calls = []
class Provider:
async def capabilities(self, *args):
calls.append("capabilities")
return Capabilities(operations={"manual"}, processing_options=set(), pipeline={})
async def ingest(self, *args):
pytest.fail("Unsupported option submitted a write")
with pytest.raises(NotImplementedError):
await Platform(settings, store, {"local": Provider()}).invoke(ADMIN, "project", resource, "manual", IngestRequest(kind="manual", title="Synthetic", content="Test"))
assert calls == ["capabilities"]
def test_install_is_opt_in_and_uses_only_operator_factory(platform, monkeypatch):
from types import SimpleNamespace
from deerflow_extension_knowledge_platform import install
_, settings, _ = platform
calls = []
class Registry:
plugins, mounted = [], []
def plugin(self, contribution):
self.plugins.append(contribution)
return True
def routers(self, routes):
self.mounted.extend(routes)
registry = Registry()
install(registry, {})
assert not registry.plugins
monkeypatch.setattr("deerflow_extension_knowledge_platform.import_module", lambda module: SimpleNamespace(WeKnoraManagement=lambda config: calls.append(config) or object()))
install(registry, settings.model_dump())
assert len(registry.plugins) == len(registry.mounted) == len(calls) == 1
assert registry.plugins[0].namespace == "knowledge-platform"
assert registry.plugins[0].frontend is None