mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
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>
This commit is contained in:
parent
2c227dc193
commit
3d4a9ca09f
19
.github/workflows/extension-package-check.yml
vendored
19
.github/workflows/extension-package-check.yml
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
59
backend/tests/test_extension_package_ci.py
Normal file
59
backend/tests/test_extension_package_ci.py
Normal file
@ -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
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user