deer-flow/backend/tests/test_extension_package_ci.py
Hyeonsang Cho 3d4a9ca09f
fix(examples): keep the extension example's tests passing as the contract grows (#7364)
* fix(examples): keep the extension example's tests passing as the contract grows

The README's package-test commands failed one test on main:
isinstance(registry, ExtensionRegistry) is False because the test's
FakeRegistry does not implement knowledge_provider(), which
extension-api 0.2.7 added to the runtime-checkable protocol. The same
assertion broke earlier when plugin() was added. Nothing in CI runs
these tests, so each addition to the contract broke them silently.

FakeRegistry now records knowledge_provider() and plugin() calls, and
the test asserts the example registers neither, as its README says.
Extension Package Check, which already runs when the contract changes,
now installs each bundled example that has a tests/ directory next to
the checkout's extension-api and runs the README's pytest and ruff
commands. A backend test pins that step and checks that every tested
example declares those tools in its dev extra.

The example README and the extensions guide now count nine
contribution kinds, including knowledge providers.

* docs(changelog): reference #7364 for the extension example test fix

* test(extensions): name the missing package-test step in the wiring pin

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-24 11:07:26 +08:00

60 lines
2.4 KiB
Python

"""Keep bundled extension package tests running when the public contract changes."""
from __future__ import annotations
import tomllib
from pathlib import Path
import pytest
import yaml
from packaging.requirements import Requirement
REPO_ROOT = Path(__file__).resolve().parents[2]
TEST_STEP = "Test bundled extension packages that ship tests"
def _workflow() -> dict:
# BaseLoader preserves GitHub Actions' `on` key rather than treating it as
# YAML 1.1's boolean True.
return yaml.load((REPO_ROOT / ".github/workflows/extension-package-check.yml").read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
def _test_step() -> dict:
steps = _workflow()["jobs"]["check-packages"]["steps"]
step = next((step for step in steps if step.get("name") == TEST_STEP), None)
assert step is not None, f"workflow step {TEST_STEP!r} not found in extension-package-check.yml"
return step
def _tested_packages() -> list[Path]:
return sorted(path.parent for path in REPO_ROOT.glob("examples/deerflow-extension-*/tests"))
@pytest.mark.parametrize("event", ["push", "pull_request"])
def test_contract_and_example_changes_trigger_the_package_check(event: str) -> None:
paths = _workflow()["on"][event]["paths"]
assert "examples/deerflow-extension-*/**" in paths
assert "backend/packages/extension-api/**" in paths
def test_package_tests_run_against_the_checkout_contract() -> None:
script = _test_step()["run"]
lines = [line.strip() for line in script.splitlines()]
assert "for package in examples/deerflow-extension-*/; do" in lines
assert '[ -d "${package}tests" ] || continue' in lines
install_contract = lines.index("uv pip install -e ../../backend/packages/extension-api")
install_package = lines.index('uv pip install -e ".[dev]"')
assert install_contract < install_package
for command in ("uv run --no-project pytest -q", "uv run --no-project ruff check .", "uv run --no-project ruff format --check ."):
assert lines.index(command) > install_package
def test_every_example_with_tests_declares_the_dev_tools_ci_runs() -> None:
packages = _tested_packages()
assert packages, "the extension example ships package tests"
for package in packages:
project = tomllib.loads((package / "pyproject.toml").read_text(encoding="utf-8"))["project"]
dev = {Requirement(value).name for value in project.get("optional-dependencies", {}).get("dev", [])}
assert {"pytest", "ruff"} <= dev, package.name