From 3d4a9ca09f79380d813c5420b3c7bdebcc5aaa0b Mon Sep 17 00:00:00 2001 From: Hyeonsang Cho Date: Sat, 24 Oct 2026 12:07:26 +0900 Subject: [PATCH] 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 --- .github/workflows/extension-package-check.yml | 19 ++++++ CHANGELOG.md | 7 +++ CHANGELOG_zh.md | 6 ++ .../harness/deerflow/extensions/AGENTS.md | 7 ++- backend/tests/test_extension_package_ci.py | 59 +++++++++++++++++++ examples/README.md | 5 +- examples/deerflow-extension-example/README.md | 11 ++-- .../tests/test_plugin.py | 15 ++++- 8 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 backend/tests/test_extension_package_ci.py diff --git a/.github/workflows/extension-package-check.yml b/.github/workflows/extension-package-check.yml index ae81d145df..828c40c33d 100644 --- a/.github/workflows/extension-package-check.yml +++ b/.github/workflows/extension-package-check.yml @@ -53,3 +53,22 @@ jobs: uv run --project backend --no-sync python scripts/check_extension.py "$package" echo "::endgroup::" done + # Each package's own tests run in an isolated environment against this + # checkout's extension-api, so a contract change that breaks them fails here. + - name: Test bundled extension packages that ship tests + shell: bash + run: | + for package in examples/deerflow-extension-*/; do + [ -d "${package}tests" ] || continue + echo "::group::Test ${package%/}" + ( + cd "$package" + uv venv --python 3.12 + uv pip install -e ../../backend/packages/extension-api + uv pip install -e ".[dev]" + uv run --no-project pytest -q + uv run --no-project ruff check . + uv run --no-project ruff format --check . + ) + echo "::endgroup::" + done diff --git a/CHANGELOG.md b/CHANGELOG.md index c5fe496b20..2352629a65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1397,6 +1397,12 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. ### Fixed +- **examples:** The extension example's package tests pass again. Its test + registry lacked `knowledge_provider()`, so it no longer matched the + `ExtensionRegistry` protocol and the README's `pytest` run failed. Extension + Package Check now also runs the tests of bundled examples that ship them, + so a contract change that breaks them fails its pull request. ([#7364]) + - **gateway:** Run requests with a malformed body now get a client error instead of `500 Internal Server Error`. A `config.configurable` or `config.context` that is not an object, an invalid `assistant_id`, a @@ -13045,3 +13051,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#7325]: https://github.com/bytedance/deer-flow/pull/7325 [#7326]: https://github.com/bytedance/deer-flow/pull/7326 [#7362]: https://github.com/bytedance/deer-flow/pull/7362 +[#7364]: https://github.com/bytedance/deer-flow/pull/7364 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index a31ff60fc9..f1d4be7fef 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -1112,6 +1112,11 @@ ### 修复 +- **示例:** 扩展示例的包测试重新通过。其测试用 registry 缺少 + `knowledge_provider()`,不再满足 `ExtensionRegistry` 协议,按 README 运行 + `pytest` 会失败。Extension Package Check 现在也会运行自带测试的内置示例的测试, + 因此破坏这些测试的契约变更会在其 PR 中失败。([#7364]) + - **网关:** 请求体格式错误的运行请求现在返回客户端错误,而不是 `500 Internal Server Error`。此前 `config.configurable` 或 `config.context` 不是对象、`assistant_id` 无效、`agent_name` 不是字符串,或 AI 消息的 `tool_calls` @@ -10413,3 +10418,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#7325]: https://github.com/bytedance/deer-flow/pull/7325 [#7326]: https://github.com/bytedance/deer-flow/pull/7326 [#7362]: https://github.com/bytedance/deer-flow/pull/7362 +[#7364]: https://github.com/bytedance/deer-flow/pull/7364 diff --git a/backend/packages/harness/deerflow/extensions/AGENTS.md b/backend/packages/harness/deerflow/extensions/AGENTS.md index 0401949422..22a06a1521 100644 --- a/backend/packages/harness/deerflow/extensions/AGENTS.md +++ b/backend/packages/harness/deerflow/extensions/AGENTS.md @@ -170,10 +170,11 @@ entry, the manager owns the controlled locked sync. The public package is `packages/extension-api/` and must never import `deerflow` or carry framework dependencies. Extensions declare any FastAPI, LangChain, or LangGraph imports -themselves. Its registry contract exposes eight contribution kinds: middleware +themselves. Its registry contract exposes nine contribution kinds: middleware contributors, task-lifecycle contributors, system-model-call observers, agent-assembly -observers, context-compaction observers, Gateway-lifetime services, eager routers, and -experimental full-stack plugins (`registry.plugin()`, see `docs/full-stack-plugins.md`). Middleware contributions declare lead/subagent scope, stable +observers, context-compaction observers, Gateway-lifetime services, eager routers, +experimental full-stack plugins (`registry.plugin()`, see `docs/full-stack-plugins.md`), +and read-only knowledge providers (`registry.knowledge_provider()`, above). Middleware contributions declare lead/subagent scope, stable order, and a semantic placement (`MODEL_LOGICAL`, `MODEL_PHYSICAL`, `TOOL_VISIBLE`, `TOOL_RAW`, or `STANDARD`) rather than a fragile list index. `extensions/stack.py` is the single final composition point; do not inject inside diff --git a/backend/tests/test_extension_package_ci.py b/backend/tests/test_extension_package_ci.py new file mode 100644 index 0000000000..1bd9d1081a --- /dev/null +++ b/backend/tests/test_extension_package_ci.py @@ -0,0 +1,59 @@ +"""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 diff --git a/examples/README.md b/examples/README.md index c936b46a94..22d3e45296 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,7 +20,10 @@ Additional examples cover [text-list classification](deerflow-extension-jev-clas package, distribution and `deerflow.extensions` entry point. For a full-stack plugin, also choose its own namespace and browser module name. 2. Follow the example's README to configure its dependencies and run its tests. - Keep package behavior tests alongside the extension. + Keep package behavior tests alongside the extension. For a bundled example + with a `tests/` directory, CI installs the package with its `dev` extra next + to this checkout's `deerflow-extension-api`, then runs `pytest`, `ruff check` + and `ruff format --check`. 3. From the DeerFlow checkout root, prepare the backend environment with `make -C backend install`, then check the package: diff --git a/examples/deerflow-extension-example/README.md b/examples/deerflow-extension-example/README.md index cde734b95e..e0a22fb78b 100644 --- a/examples/deerflow-extension-example/README.md +++ b/examples/deerflow-extension-example/README.md @@ -4,14 +4,15 @@ For the shared edit → test → packaging check → install workflow, see the [existing-example guide](../README.md). This directory is a compact, standalone Python package showing five of the -eight DeerFlow extension contribution kinds: middleware, task lifecycle, +nine DeerFlow extension contribution kinds: middleware, task lifecycle, system-model observer, service, and router. It depends on the public `deerflow-extension-api` contract and never imports `deerflow.*` or `app.*`. It does not register an agent-assembly observer, a context-compaction observer, -or a full-stack plugin. The observers are covered in the +a knowledge provider, or a full-stack plugin. The observers are covered in the [Lifecycle and Observers](../../frontend/src/content/en/harness/extensions/observers.mdx) -guide, and [`deerflow-extension-bookmarks`](../deerflow-extension-bookmarks/) +guide, [`deerflow-extension-knowledge`](../deerflow-extension-knowledge/) shows +a knowledge provider, and [`deerflow-extension-bookmarks`](../deerflow-extension-bookmarks/) shows a plugin. The contract package intentionally has no framework dependencies. An extension @@ -92,7 +93,9 @@ uv run --no-project ruff format --check . ``` The tests use only the public contract plus this package's declared dependencies; -the DeerFlow harness and Gateway application are not imported. +the DeerFlow harness and Gateway application are not imported. CI runs these +commands whenever this package or `deerflow-extension-api` changes, so a contract +addition that the test registry does not implement fails the pull request. ## Install and load it in DeerFlow diff --git a/examples/deerflow-extension-example/tests/test_plugin.py b/examples/deerflow-extension-example/tests/test_plugin.py index ac007b0334..d13935863f 100644 --- a/examples/deerflow-extension-example/tests/test_plugin.py +++ b/examples/deerflow-extension-example/tests/test_plugin.py @@ -26,10 +26,9 @@ from deerflow_extension_example import install class FakeRegistry: - def plugin(self, contribution: Any) -> bool: - return False - def __init__(self) -> None: + self.plugins: list[Any] = [] + self.knowledge_providers: list[tuple[str, Any]] = [] self.middleware_contributors: list[Any] = [] self.task_lifecycle_contributors: list[Any] = [] self.system_model_observers: list[Any] = [] @@ -38,6 +37,14 @@ class FakeRegistry: self.services: list[Any] = [] self.contributed_routers: list[Any] = [] + def plugin(self, contribution: Any) -> bool: + self.plugins.append(contribution) + return True + + def knowledge_provider(self, name: str, provider: Any) -> bool: + self.knowledge_providers.append((name, provider)) + return True + def middlewares(self, contributor: Any) -> None: self.middleware_contributors.append(contributor) @@ -84,6 +91,8 @@ def test_install_registers_the_five_demonstrated_contribution_kinds() -> None: # The README names these as not demonstrated here. assert registry.agent_assembly_observers == [] assert registry.context_compaction_observers == [] + assert registry.knowledge_providers == [] + assert registry.plugins == [] assert [route.path for route in registry.contributed_routers[0].routes] == ["/api/extension-example/stats"] assert install.__deerflow_api__ == "0.2.0" assert install.__deerflow_name__ == "example"