deer-flow/backend/tests/test_managed_subagent_store.py
Totoro 8d11aeb97b
feat(subagents): configure thinking and reasoning effort for native workers (#7668)
* feat(subagents): add thinking_enabled and reasoning_effort config for sub-agents (#4875)

(cherry picked from commit b1083728b23504cb0f1d1a463f8db70c5ec1c5a7)

* fix: resolve CI failures for subagent thinking config PR

- Fix UnboundLocalError in task_tool.py: initialize parent_thinking_enabled
  and parent_reasoning_effort before the if runtime block
- Remove unused imports from test_subagent_thinking_config.py
  (SimpleNamespace, pytest, resolve_subagent_model_name)
- Fix import sorting in subagents_config.py and test file
- Add reasoning_effort: None to executor test assertion

(cherry picked from commit 187851e307b871937e48bb67ae1b34b8b69bf92d)

* fix: address review comments - remove parent inheritance, supports_thinking clamp, Literal type

- Remove parent_thinking_enabled/parent_reasoning_effort from metadata
  extraction in task_tool.py (backward compatible: subagents only think
  when explicitly configured)
- Add supports_thinking clamp in _create_agent to prevent cross-model
  mismatch errors
- Change SubagentConfig.reasoning_effort to Literal type
- Fix sandbox_state assignment that was accidentally removed
- Fix syntax errors in subagents_config.py (missing closing parens)
- Update all docstrings/comments from "inherit" to "no override"
- Simplify test helper and remove parent inheritance tests

(cherry picked from commit 74f9c659ef4a98bd1756109929ab0748fab82317)

* feat(subagents): adapt reasoning settings to current native workers

* fix(subagents): retain model context on reasoning rejection

* fix(subagents): preserve model-factory reasoning diagnostics

---------

Co-authored-by: Neon549 <neon549@users.noreply.github.com>
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-29 22:26:12 +08:00

161 lines
6.1 KiB
Python

"""Persistence coverage for administrator-managed subagents."""
from __future__ import annotations
import json
from datetime import UTC, datetime, timedelta
from types import SimpleNamespace
import pytest
from sqlalchemy import create_engine, select
from deerflow.persistence.base import Base
from deerflow.persistence.managed_subagents import (
ManagedSubagentDefinition,
ManagedSubagentExistsError,
make_managed_subagent_store,
)
from deerflow.persistence.managed_subagents.file import FileManagedSubagentStore
from deerflow.persistence.managed_subagents.model import ManagedSubagentRow
from deerflow.persistence.managed_subagents.sql import SqlManagedSubagentStore
def _definition(name: str = "researcher", **changes) -> ManagedSubagentDefinition:
return ManagedSubagentDefinition(
name=name,
description="Researches a bounded topic",
system_prompt="You are a research specialist.",
**changes,
)
@pytest.fixture()
def file_store(tmp_path, monkeypatch):
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path))
monkeypatch.setattr("deerflow.config.paths._paths", None)
return FileManagedSubagentStore()
@pytest.fixture()
def sql_store(tmp_path):
url = f"sqlite:///{tmp_path}/managed.db"
engine = create_engine(url)
Base.metadata.create_all(engine, tables=[ManagedSubagentRow.__table__])
engine.dispose()
return SqlManagedSubagentStore(url)
@pytest.mark.parametrize("store_fixture", ["file_store", "sql_store"])
def test_crud_and_signature(request, store_fixture):
store = request.getfixturevalue(store_fixture)
empty_signature = store.signature()
definition = _definition(thinking_enabled=True, reasoning_effort="xhigh")
store.create(definition)
assert store.get("RESEARCHER") == definition
assert [item.name for item in store.list()] == ["researcher"]
created_signature = store.signature()
assert created_signature != empty_signature
updated = definition.model_copy(update={"enabled": False, "max_turns": 75})
store.update(updated)
assert store.get("researcher").enabled is False
assert store.get("researcher").max_turns == 75
assert store.get("researcher").thinking_enabled is True
assert store.get("researcher").reasoning_effort == "xhigh"
updated_signature = store.signature()
assert updated_signature != created_signature
assert store.delete("researcher") is True
assert store.signature() != updated_signature
assert store.delete("researcher") is False
with pytest.raises(FileNotFoundError):
store.get("researcher")
@pytest.mark.parametrize("store_fixture", ["file_store", "sql_store"])
def test_legacy_definition_without_reasoning_fields_keeps_defaults(request, store_fixture, tmp_path):
store = request.getfixturevalue(store_fixture)
legacy = {"name": "researcher", "description": "Research", "system_prompt": "Research the task."}
if store_fixture == "file_store":
root = tmp_path / "managed-subagents"
root.mkdir()
(root / "researcher.json").write_text(json.dumps(legacy), encoding="utf-8")
else:
with store._Session() as session:
session.add(ManagedSubagentRow(id="legacy", name="researcher", definition=legacy))
session.commit()
definition = store.get("researcher")
assert definition.thinking_enabled is None
assert definition.reasoning_effort is None
assert store.list() == [definition]
def test_sql_signature_detects_update_behind_existing_max_timestamp(sql_store):
sql_store.create(_definition("ahead"))
sql_store.create(_definition("writer"))
with sql_store._Session() as session:
ahead = session.execute(select(ManagedSubagentRow).where(ManagedSubagentRow.name == "ahead")).scalar_one()
ahead.updated_at = datetime.now(UTC) + timedelta(minutes=5)
session.commit()
before = sql_store.signature()
writer = sql_store.get("writer").model_copy(update={"description": "Updated by a trailing node"})
sql_store.update(writer)
assert sql_store.get("writer").description == "Updated by a trailing node"
assert sql_store.signature() != before
@pytest.mark.parametrize("store_fixture", ["file_store", "sql_store"])
def test_duplicate_create_is_a_conflict(request, store_fixture):
store = request.getfixturevalue(store_fixture)
store.create(_definition())
with pytest.raises(ManagedSubagentExistsError):
store.create(_definition())
def test_worker_boundary_is_always_enforced():
definition = _definition(disallowed_tools=[])
assert {"task", "ask_clarification", "present_files"}.issubset(definition.disallowed_tools)
def test_file_store_uses_one_atomic_file_per_definition(file_store, tmp_path):
file_store.create(_definition("planner"))
file_store.create(_definition("writer"))
root = tmp_path / "managed-subagents"
assert sorted(path.name for path in root.iterdir()) == ["planner.json", "writer.json"]
assert not list(root.glob("*.tmp"))
def test_file_store_skips_one_corrupt_definition_without_hiding_valid_entries(file_store, tmp_path):
file_store.create(_definition("planner"))
root = tmp_path / "managed-subagents"
(root / "broken.json").write_text("{not-json", encoding="utf-8")
assert [item.name for item in file_store.list()] == ["planner"]
def test_store_factory_follows_agent_storage_backend(tmp_path):
file_config = SimpleNamespace(agent_storage=SimpleNamespace(backend="file"))
assert isinstance(make_managed_subagent_store(file_config), FileManagedSubagentStore)
db_config = SimpleNamespace(
agent_storage=SimpleNamespace(backend="db"),
database=SimpleNamespace(
backend="sqlite",
app_sync_sqlalchemy_url=f"sqlite:///{tmp_path}/factory.db",
),
)
assert isinstance(make_managed_subagent_store(db_config), SqlManagedSubagentStore)
def test_store_factory_rejects_memory_database():
config = SimpleNamespace(
agent_storage=SimpleNamespace(backend="db"),
database=SimpleNamespace(backend="memory"),
)
with pytest.raises(ValueError, match="sqlite.*postgres"):
make_managed_subagent_store(config)