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"