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>
This commit is contained in:
Rei 2026-11-14 09:12:25 +08:00 committed by GitHub
parent cee54cdd49
commit 9cb2bde557
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 365 additions and 548 deletions

View File

@ -0,0 +1,104 @@
name: Bundled Extension Tests
# Bundled extension examples own their behavior suites, which carry the
# `bundled_extension` marker. The core backend shards exclude them so an
# optional example cannot fail an unrelated core PR; this workflow runs them
# whenever an example, the extension contract, the host they load into or the
# bundled tests change.
#
# The filter is deliberately bounded: the marked modules import `deerflow.*`
# (packages) and `app.gateway.*`, so only the Gateway subtree is listed rather
# than all of `backend/app`. Test files are listed one by one instead of
# `backend/tests/**`, and `test_bundled_extension_ci.py` fails when a module
# carries the marker without appearing here, so this list cannot drift.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'examples/deerflow-extension-*/**'
- 'backend/packages/**'
- 'backend/app/gateway/**'
- 'backend/tests/conftest.py'
- 'backend/tests/support/**'
- 'backend/tests/test_agent_teams_plugin.py'
- 'backend/tests/test_bookmark_plugin.py'
- 'backend/tests/test_bundled_extension_ci.py'
- 'backend/tests/test_jev_classify_extension.py'
- 'backend/tests/test_jev_context_extension.py'
- 'backend/tests/test_jev_result_screening_extension.py'
- 'backend/tests/test_jev_screening_pipeline.py'
- 'backend/tests/test_jev_screening_policy.py'
- 'backend/tests/test_knowledge_center_extension.py'
- 'backend/tests/test_knowledge_management_extension.py'
- 'backend/tests/test_knowledge_platform_extension.py'
- 'backend/tests/test_lightrag_relations_extension.py'
- 'backend/tests/test_report_sections_extension.py'
- 'backend/tests/test_retrieval_plugin_artifacts.py'
- 'backend/tests/test_weknora_extension.py'
- 'backend/tests/test_workflow_plugin.py'
- 'backend/pyproject.toml'
- 'backend/uv.lock'
- 'backend/Makefile'
- '.github/workflows/bundled-extension-tests.yml'
push:
branches: [main, "*-dev"]
paths:
- 'examples/deerflow-extension-*/**'
- 'backend/packages/**'
- 'backend/app/gateway/**'
- 'backend/tests/conftest.py'
- 'backend/tests/support/**'
- 'backend/tests/test_agent_teams_plugin.py'
- 'backend/tests/test_bookmark_plugin.py'
- 'backend/tests/test_bundled_extension_ci.py'
- 'backend/tests/test_jev_classify_extension.py'
- 'backend/tests/test_jev_context_extension.py'
- 'backend/tests/test_jev_result_screening_extension.py'
- 'backend/tests/test_jev_screening_pipeline.py'
- 'backend/tests/test_jev_screening_policy.py'
- 'backend/tests/test_knowledge_center_extension.py'
- 'backend/tests/test_knowledge_management_extension.py'
- 'backend/tests/test_knowledge_platform_extension.py'
- 'backend/tests/test_lightrag_relations_extension.py'
- 'backend/tests/test_report_sections_extension.py'
- 'backend/tests/test_retrieval_plugin_artifacts.py'
- 'backend/tests/test_weknora_extension.py'
- 'backend/tests/test_workflow_plugin.py'
- 'backend/pyproject.toml'
- 'backend/uv.lock'
- 'backend/Makefile'
- '.github/workflows/bundled-extension-tests.yml'
permissions:
contents: read
concurrency:
group: bundled-extension-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
bundled-extension-tests:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- uses: astral-sh/setup-uv@v7
with:
# Must match backend/Dockerfile's UV_IMAGE tag; pinned by backend/tests/test_ci_uv_version_pin.py
version: "0.11.1"
- name: Install backend dependencies
working-directory: backend
run: uv sync --group dev
- name: Run bundled extension behavior tests
working-directory: backend
run: make test-bundled-extensions

View File

@ -350,7 +350,7 @@ before review.
python -m pip install -r tests/skills/requirements.txt
python -m pytest tests/skills -q
# Default backend tests (four parallel shards; excludes live and blocking-I/O tests)
# Default backend tests (four parallel shards; excludes live, blocking-I/O and bundled-extension tests)
cd backend
make test
@ -360,6 +360,9 @@ make test TEST_JOBS=1
# Strict blocking-I/O tests
make test-blocking-io
# Behavior suites of the bundled extension examples
make test-bundled-extensions
# Live DeerFlowClient integration tests (explicit opt-in)
# Requires a valid root config.yaml and API credentials.
make test-live
@ -388,6 +391,7 @@ must also set `DEER_FLOW_RUN_LIVE_TESTS=1`.
Every pull request triggers the following CI workflows:
- **Backend unit tests** — [.github/workflows/backend-unit-tests.yml](.github/workflows/backend-unit-tests.yml)
- **Bundled extension tests** — [.github/workflows/bundled-extension-tests.yml](.github/workflows/bundled-extension-tests.yml) (triggered by example packages, `backend/packages/**`, `backend/app/gateway/**` and the marked test modules)
- **Frontend unit tests** — [.github/workflows/frontend-unit-tests.yml](.github/workflows/frontend-unit-tests.yml)
- **Frontend E2E tests** — [.github/workflows/e2e-tests.yml](.github/workflows/e2e-tests.yml) (triggered only when `frontend/` files change)
@ -404,13 +408,25 @@ setup also verifies SSL and SQLite imports before launching fixture gateways.
Backend `make test` runs four duration-balanced shards concurrently and reports
failure if any shard fails. Use `make test TEST_JOBS=1` to run the shards sequentially.
It excludes live external-API and blocking-I/O coverage.
It excludes live external-API, blocking-I/O and bundled-extension coverage.
Run `cd backend && make test-blocking-io` for strict blocking-I/O checks.
Maintainers can run the real `DeerFlowClient` suite with `cd backend && make test-live`.
This command requires a valid root `config.yaml` and API credentials.
It can incur API costs and create local sandboxes, artifacts, or files.
Direct pytest runs additionally require `DEER_FLOW_RUN_LIVE_TESTS=1`.
Bundled extension examples keep their behavior suites in `backend/tests/` under
the `bundled_extension` pytest marker. The core shards exclude them so an
optional example cannot fail an unrelated core PR, and a separate CI workflow
runs them when an example, the public extension contract or the bundled tests
change. Run `cd backend && make test-bundled-extensions` to exercise them
locally. Tests that validate a host mechanism — loading, registration, the
public extension API and authorization — stay in the core shards even when an
example package is the payload. The host/extension compatibility checks (an
extension requiring a newer API than the host provides, authorization of
contributed routes) live in `backend/tests/test_extension_host_contracts.py` so
a broken compatibility guarantee fails core CI.
Regression coverage includes Docker sandbox mode detection and provisioner kubeconfig-path handling tests in `backend/tests/`.
Backend blocking-IO diagnostics are available from the repository root with
`make detect-blocking-io`: it statically scans backend business code for

File diff suppressed because one or more lines are too long

View File

@ -29,6 +29,7 @@ Read the guide for an area before you change it.
```bash
make test # offline suite in parallel shards
make test-blocking-io # strict blocking-I/O suite
make test-bundled-extensions # behavior suites of bundled examples (not in make test)
make lint
make format
make test-live # calls real APIs; needs DEER_FLOW_RUN_LIVE_TESTS=1

View File

@ -22,7 +22,7 @@ gateway:
# Run all four duration-balanced shards locally; wait for every shard on failure.
TEST_JOBS ?= 4
TEST_SHARD_TARGETS := test-shard-1 test-shard-2 test-shard-3 test-shard-4
.PHONY: test test-shard $(TEST_SHARD_TARGETS)
.PHONY: test test-shard test-bundled-extensions $(TEST_SHARD_TARGETS)
test:
$(MAKE) --no-print-directory -k -j$(TEST_JOBS) $(TEST_SHARD_TARGETS)
@ -49,13 +49,21 @@ DURATIONS_FILE ?= .test_durations
test-shard:
@test -f "$(DURATIONS_FILE)" || \
(echo "error: $(DURATIONS_FILE) is missing; run 'make test-shard-durations' to regenerate the duration baseline" >&2; exit 1)
PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run pytest -m "not live" --ignore=tests/blocking_io --splits $(SPLITS) --group $(GROUP) --splitting-algorithm least_duration --durations-path=$(DURATIONS_FILE) tests/ -q
PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run pytest -m "not live and not bundled_extension" --ignore=tests/blocking_io --splits $(SPLITS) --group $(GROUP) --splitting-algorithm least_duration --durations-path=$(DURATIONS_FILE) tests/ -q
# Regenerate .test_durations from the full offline (non-live, non-blocking-I/O)
# suite so duration-aware sharding reflects real wall-clock cost. Run locally or
# in a periodic job, then commit the refreshed file.
# Regenerate .test_durations from the offline core suite (non-live,
# non-blocking-I/O, non-bundled-extension) so duration-aware sharding reflects
# real wall-clock cost. Run locally or in a periodic job, then commit the
# refreshed file.
test-shard-durations:
PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run pytest -m "not live" --ignore=tests/blocking_io --store-durations --clean-durations --durations-path=$(DURATIONS_FILE) tests/ -q
PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run pytest -m "not live and not bundled_extension" --ignore=tests/blocking_io --store-durations --clean-durations --durations-path=$(DURATIONS_FILE) tests/ -q
# Bundled extension examples own their behavior suites; the core shards exclude
# them so an optional example cannot fail an unrelated core PR. The extension CI
# workflow runs this target when an example, the extension contract or the
# bundled tests themselves change.
test-bundled-extensions:
PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run pytest -m "bundled_extension and not live" tests/ -q
lint:
uv run ruff check .

View File

@ -89,6 +89,7 @@ markers = [
"allow_blocking_io: opt out of the strict Blockbuster gate in tests/blocking_io/",
"integration: tests that require an external service (e.g. Redis); skipped when unavailable",
"live: tests that call real external APIs and require explicit opt-in",
"bundled_extension: behavior tests owned by a bundled extension example; excluded from the core shards and run by the bundled extension CI job",
]
[tool.uv]

View File

@ -62,6 +62,18 @@ with a reason when the binary is missing.
- **Circular imports are solved in `conftest.py`** with a `sys.modules` stub; see
the existing entry for `deerflow.subagents.executor`.
## Bundled extension behavior
A module that validates a bundled example's own behavior carries
`pytest.mark.bundled_extension` at module level, so the core shards skip it and
the bundled extension CI runs it. Host mechanisms stay in the core shards even
when an example package is the payload: loading and registration, the public
extension API, authorization, routing and runtime. Host/extension compatibility
checks live in `test_extension_host_contracts.py`, so a broken compatibility
guarantee still fails core CI, and `test_bundled_extension_ci.py` keeps the
workflow's test-file list in step with the marker. Run the bundled suites with
`make test-bundled-extensions`.
## Blocking-I/O tests
Every test under `blocking_io/` runs with a detector that fails on a blocking

View File

@ -11,20 +11,7 @@ from deerflow_extension_api.plugins import ActionContext, ToolContext
from deerflow.extensions.loader import ExtensionSpec, load_extensions
@pytest.mark.parametrize("host_version", ["0.2.10", "0.2.11"])
def test_team_artifact_extension_requires_a_host_with_artifact_contracts(tmp_path, monkeypatch, host_version):
monkeypatch.syspath_prepend(str(Path(__file__).resolve().parents[2] / "examples/deerflow-extension-agent-teams"))
monkeypatch.setattr("deerflow.extensions.loader.API_VERSION", host_version)
loaded, diagnostics = load_extensions([ExtensionSpec(use="deerflow_extension_agent_teams:install", config={"enabled": True, "storage_path": str(tmp_path / "teams.sqlite")})])
if host_version == "0.2.10":
assert not loaded.plugins
assert len(diagnostics) == 1
assert "requires extension-api 0.2.11, host provides 0.2.10" in diagnostics[0].message
else:
assert not diagnostics
assert len(loaded.plugins) == 1
pytestmark = pytest.mark.bundled_extension
def test_team_artifact_package_dependency_rejects_the_pre_artifact_api():

View File

@ -14,6 +14,8 @@ from langgraph.prebuilt import ToolNode
from deerflow.extensions.loader import ExtensionSpec, load_extensions
from deerflow.extensions.plugin_tools import build_plugin_tools
pytestmark = pytest.mark.bundled_extension
@pytest.fixture
def bookmarks(tmp_path, monkeypatch):

View File

@ -0,0 +1,114 @@
"""Keep the bundled extension CI wired to the marker and the Makefile targets."""
from __future__ import annotations
import ast
import tomllib
from pathlib import Path
import pytest
import yaml
REPO_ROOT = Path(__file__).resolve().parents[2]
TESTS_DIR = REPO_ROOT / "backend/tests"
WORKFLOW = REPO_ROOT / ".github/workflows/bundled-extension-tests.yml"
MAKEFILE = REPO_ROOT / "backend/Makefile"
MARKER = "bundled_extension"
# The marked suites load the bundled examples into these host subtrees, so a
# change there has to run them even though the core shards no longer do.
PAYLOAD_PATHS = [
"examples/deerflow-extension-*/**",
"backend/packages/**",
"backend/app/gateway/**",
]
# Shared test infrastructure the marked modules may use.
SHARED_TEST_PATHS = [
"backend/tests/conftest.py",
"backend/tests/support/**",
]
def _workflow() -> dict:
# BaseLoader preserves GitHub Actions' `on` key rather than treating it as
# YAML 1.1's boolean True.
return yaml.load(WORKFLOW.read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
def _marked_modules() -> set[str]:
# Parse the assignment instead of grepping: this file mentions the marker in
# strings, and a marked module may live in a subdirectory.
marked = set()
for path in TESTS_DIR.rglob("test_*.py"):
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in tree.body:
if isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == "pytestmark" for target in node.targets):
if "bundled_extension" in ast.dump(node.value):
marked.add(path.relative_to(TESTS_DIR).as_posix())
return marked
@pytest.mark.parametrize("event", ["push", "pull_request"])
def test_each_event_triggers_on_the_bundled_payloads(event: str) -> None:
paths = _workflow()["on"][event]["paths"]
for path in PAYLOAD_PATHS:
assert path in paths, f"{path} is missing from the {event} paths filter"
@pytest.mark.parametrize("event", ["push", "pull_request"])
def test_each_marked_module_is_listed_in_the_trigger_paths(event: str) -> None:
paths = _workflow()["on"][event]["paths"]
marked = _marked_modules()
assert marked, "no module carries the marker"
for path in SHARED_TEST_PATHS:
assert path in paths, f"the {event} filter must cover {path}"
for module in sorted(marked):
assert f"backend/tests/{module}" in paths, f"{module} carries the marker but is not in the {event} trigger paths"
@pytest.mark.parametrize("event", ["push", "pull_request"])
def test_the_filter_does_not_cover_all_of_the_backend(event: str) -> None:
paths = _workflow()["on"][event]["paths"]
# Broad backend subtrees would run the bundled suites for unrelated Gateway
# or test changes; the marked modules only import app.gateway.
assert "backend/tests/**" not in paths
assert "backend/app/**" not in paths
assert "backend/app/gateway/**" in paths
def test_the_workflow_supersedes_in_flight_runs() -> None:
concurrency = _workflow()["concurrency"]
# A rapid push must supersede the in-flight run instead of queuing another
# full dependency install, as the sibling workflows do.
assert concurrency["group"].startswith("bundled-extension-tests-")
assert "${{ github.event.pull_request.number || github.ref }}" in concurrency["group"]
assert concurrency["cancel-in-progress"] == "true"
def test_the_job_runs_the_bundled_target_in_the_backend() -> None:
steps = _workflow()["jobs"]["bundled-extension-tests"]["steps"]
step = next((item for item in steps if item.get("run") == "make test-bundled-extensions"), None)
assert step is not None, "the workflow must run make test-bundled-extensions"
assert step["working-directory"] == "backend"
def test_core_and_bundled_selections_stay_complementary() -> None:
makefile = MAKEFILE.read_text(encoding="utf-8")
# Core shards and their duration baseline exclude the marker; the bundled
# target selects exactly it. Renaming either side must fail here.
assert makefile.count('-m "not live and not bundled_extension"') == 2
assert makefile.count('-m "bundled_extension and not live"') == 1
def test_marker_is_registered() -> None:
project = tomllib.loads((REPO_ROOT / "backend/pyproject.toml").read_text(encoding="utf-8"))
markers = project["tool"]["pytest"]["ini_options"]["markers"]
assert any(marker.startswith(f"{MARKER}:") for marker in markers), f"{MARKER} must be a registered pytest marker"

View File

@ -204,7 +204,7 @@ def test_make_targets_keep_default_tests_offline_and_support_live_opt_in() -> No
default_command = _dry_run_make_target("test")
live_command = _dry_run_make_target("test-live")
assert 'pytest -m "not live"' in default_command
assert 'pytest -m "not live and not bundled_extension"' in default_command
assert "--ignore=tests/blocking_io" in default_command
assert "tests/" in default_command
assert LIVE_OPT_IN not in default_command
@ -256,7 +256,7 @@ def test_ci_unit_test_workflow_runs_duration_aware_shards() -> None:
assert "--splits 6" in command
assert "--group 6" in command
assert 'pytest -m "not live"' in command
assert 'pytest -m "not live and not bundled_extension"' in command
assert "--ignore=tests/blocking_io" in command
assert "--splitting-algorithm least_duration" in command
assert "--durations-path=.test_durations" in command
@ -267,7 +267,7 @@ def test_ci_unit_test_workflow_runs_duration_aware_shards() -> None:
# The full-suite entry point stays offline-only.
full_command = _dry_run_make_target("test")
assert 'pytest -m "not live"' in full_command
assert 'pytest -m "not live and not bundled_extension"' in full_command
assert "--ignore=tests/blocking_io" in full_command
assert LIVE_OPT_IN not in full_command

View File

@ -0,0 +1,66 @@
"""Host/extension compatibility checks that core CI must own.
The bundled examples' own suites live behind the `bundled_extension` marker, so
an example regression cannot red an unrelated core PR. These checks are the
other side of that split: they assert what the host promises any extension
(API-version gates and the authorization gate on contributed routes), and they
belong to the core shards even when an example package is the payload.
"""
from pathlib import Path
from types import SimpleNamespace
import httpx
import pytest
from fastapi import FastAPI
from deerflow.extensions.loader import ExtensionSpec, load_extensions
@pytest.mark.parametrize("host_version", ["0.2.10", "0.2.11"])
def test_extension_requiring_a_newer_host_api_is_rejected(tmp_path, monkeypatch, host_version):
monkeypatch.syspath_prepend(str(Path(__file__).resolve().parents[2] / "examples/deerflow-extension-agent-teams"))
monkeypatch.setattr("deerflow.extensions.loader.API_VERSION", host_version)
loaded, diagnostics = load_extensions([ExtensionSpec(use="deerflow_extension_agent_teams:install", config={"enabled": True, "storage_path": str(tmp_path / "teams.sqlite")})])
if host_version == "0.2.10":
assert not loaded.plugins
assert len(diagnostics) == 1
assert "requires extension-api 0.2.11, host provides 0.2.10" in diagnostics[0].message
else:
assert not diagnostics
assert len(loaded.plugins) == 1
@pytest.mark.asyncio
@pytest.mark.parametrize("method", ["GET", "POST"])
@pytest.mark.parametrize("root_path", ["", "/proxy"])
async def test_stale_browser_viewer_never_reaches_contributed_routes(monkeypatch, method, root_path):
from app.gateway.auth_middleware import AuthMiddleware
async def user(request):
return SimpleNamespace(id="bob", system_role="admin")
async def permissions(*args, **kwargs):
return []
monkeypatch.setattr("app.gateway.auth_middleware.is_auth_disabled", lambda: False)
monkeypatch.setattr("app.gateway.deps.get_current_user_from_request", user)
monkeypatch.setattr("app.gateway.auth_middleware.resolve_route_permissions", permissions)
app = FastAPI()
app.add_middleware(AuthMiddleware)
calls = []
@app.api_route("/api/extensions/example/documents", methods=["GET", "POST"])
async def contributed():
calls.append("provider")
return {"ok": True}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, root_path=root_path), base_url="http://test", cookies={"access_token": "session-b"}) as client:
path = root_path + "/api/extensions/example/documents"
stale = await client.request(method, path, headers={"X-Deerflow-Plugin-Viewer": "alice"})
assert stale.status_code == 409
assert calls == []
for header in ({"X-Deerflow-Plugin-Viewer": "bob"}, {}):
assert (await client.request(method, path, headers=header)).status_code == 200
assert calls == ["provider", "provider"]

View File

@ -16,6 +16,8 @@ from langgraph.prebuilt import ToolNode
from deerflow.extensions.loader import ExtensionSpec, load_extensions
from deerflow.extensions.plugin_tools import build_plugin_tools, plugin_tool_name
pytestmark = pytest.mark.bundled_extension
EXAMPLE = Path(__file__).resolve().parents[2] / "examples/deerflow-extension-jev-classify"
REAL_ASYNC_CLIENT = httpx.AsyncClient
KEY = "test-only-not-a-real-key"

View File

@ -29,6 +29,8 @@ from deerflow.config.sandbox_config import SandboxConfig
from deerflow.extensions.loader import ExtensionSpec, load_extensions
from deerflow.extensions.stack import compose_with_extensions
pytestmark = pytest.mark.bundled_extension
@pytest.fixture
def jev(monkeypatch):

View File

@ -28,6 +28,8 @@ from deerflow.extensions.anchors import outermost
from deerflow.extensions.injection import inject_middlewares
from deerflow.extensions.loader import ExtensionSpec, load_extensions
pytestmark = pytest.mark.bundled_extension
EXAMPLE = Path(__file__).resolve().parents[2] / "examples/deerflow-extension-jev-screening"
PACKAGE = EXAMPLE / "deerflow_extension_jev_screening"
ENTRY = "deerflow_extension_jev_screening:install"

View File

@ -36,6 +36,8 @@ from deerflow.config.pii_redaction_config import PiiRedactionConfig
from deerflow.config.sandbox_config import SandboxConfig
from deerflow.extensions.loader import ExtensionSpec, load_extensions
pytestmark = pytest.mark.bundled_extension
EXAMPLE = Path(__file__).resolve().parents[2] / "examples/deerflow-extension-jev-screening"
REAL_CLIENT = httpx.AsyncClient
WARNING = "[Potential instruction addressed to the assistant"

View File

@ -18,6 +18,8 @@ from deerflow.extensions.anchors import outermost
from deerflow.extensions.injection import inject_middlewares
from deerflow.extensions.loader import ExtensionSpec, load_extensions
pytestmark = pytest.mark.bundled_extension
EXAMPLE = Path(__file__).resolve().parents[2] / "examples/deerflow-extension-jev-screening"

View File

@ -6,6 +6,8 @@ import pytest
from deerflow.extensions.loader import ExtensionSpec, load_extensions
pytestmark = pytest.mark.bundled_extension
@pytest.fixture
def load_center(monkeypatch):
@ -36,42 +38,3 @@ def test_page_config_cannot_introduce_credentials_or_provider_configuration(load
loaded, diagnostics = load_center(config)
assert not loaded.plugins
assert diagnostics
@pytest.mark.asyncio
@pytest.mark.parametrize("method", ["GET", "POST"])
@pytest.mark.parametrize("root_path", ["", "/proxy"])
async def test_stale_browser_viewer_never_reaches_contributed_routes(monkeypatch, method, root_path):
from types import SimpleNamespace
import httpx
from fastapi import FastAPI
from app.gateway.auth_middleware import AuthMiddleware
async def user(request):
return SimpleNamespace(id="bob", system_role="admin")
async def permissions(*args, **kwargs):
return []
monkeypatch.setattr("app.gateway.auth_middleware.is_auth_disabled", lambda: False)
monkeypatch.setattr("app.gateway.deps.get_current_user_from_request", user)
monkeypatch.setattr("app.gateway.auth_middleware.resolve_route_permissions", permissions)
app = FastAPI()
app.add_middleware(AuthMiddleware)
calls = []
@app.api_route("/api/extensions/example/documents", methods=["GET", "POST"])
async def contributed():
calls.append("provider")
return {"ok": True}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, root_path=root_path), base_url="http://test", cookies={"access_token": "session-b"}) as client:
path = root_path + "/api/extensions/example/documents"
stale = await client.request(method, path, headers={"X-Deerflow-Plugin-Viewer": "alice"})
assert stale.status_code == 409
assert calls == []
for header in ({"X-Deerflow-Plugin-Viewer": "bob"}, {}):
assert (await client.request(method, path, headers=header)).status_code == 200
assert calls == ["provider", "provider"]

View File

@ -7,6 +7,8 @@ import httpx
import pytest
from deerflow_extension_api.auth import ExtensionPrincipal
pytestmark = pytest.mark.bundled_extension
@pytest.fixture
def management(monkeypatch):

View File

@ -6,6 +6,8 @@ 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):

View File

@ -9,6 +9,8 @@ import pytest
from deerflow.knowledge import KnowledgeRequestContext, format_result, sources_from_messages
pytestmark = pytest.mark.bundled_extension
@pytest.fixture
def graph(monkeypatch):

View File

@ -80,7 +80,7 @@ def test_make_test_runs_all_four_shards_and_reports_any_failure(test_project: Pa
for group in range(1, 5):
args = json.loads((test_project / f"{group}.started").read_text(encoding="utf-8"))
assert args[:3] == ["run", "pytest", "-m"]
assert args[args.index("-m") + 1] == "not live"
assert args[args.index("-m") + 1] == "not live and not bundled_extension"
assert "--ignore=tests/blocking_io" in args
assert args[args.index("--splits") + 1] == "4"
assert args[args.index("--group") + 1] == str(group)

View File

@ -10,6 +10,8 @@ from deerflow_extension_api.plugins import ToolContext
from deerflow.extensions.loader import ExtensionSpec, load_extensions
from deerflow.extensions.plugin_tools import build_plugin_tools
pytestmark = pytest.mark.bundled_extension
EXAMPLE = Path(__file__).resolve().parents[2] / "examples/deerflow-extension-report-sections"
OUTLINE = "# 2026 年新能源汽车行业研究报告\n\n本报告分五章。"

View File

@ -18,6 +18,8 @@ from deerflow.runtime.checkpoint_mode import inject_checkpoint_mode
from deerflow.runtime.checkpoint_state import CheckpointStateAccessor
from deerflow.runtime.serialization import serialize_channel_values_for_api
pytestmark = pytest.mark.bundled_extension
@pytest.fixture
def retrieval_plugin(monkeypatch):

View File

@ -8,6 +8,8 @@ import pytest
from deerflow.knowledge import KnowledgeRequestContext, format_result, sources_from_messages
pytestmark = pytest.mark.bundled_extension
@pytest.fixture
def adapter(monkeypatch):

View File

@ -16,7 +16,10 @@ from test_extension_agent_runs import runtime as _runtime_fixture
from deerflow.extensions.loader import ExtensionSpec, load_extensions
pytestmark = pytest.mark.skipif(os.name != "posix", reason="Workflow worker execution requires POSIX")
pytestmark = [
pytest.mark.bundled_extension,
pytest.mark.skipif(os.name != "posix", reason="Workflow worker execution requires POSIX"),
]
ROOT = Path(__file__).resolve().parents[2]