mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* feat(skills): add guarded evolution host APIs * docs(agents): avoid growing saturated guidance chains * fix(skills): address mutation host review findings * fix(skills): order name fence before projection lock * fix(skills): preserve composite toggle lock order * fix(skills): address catalog and pagination review feedback * docs: keep subagent guidance within merged instruction budget --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
157 lines
6.8 KiB
Python
157 lines
6.8 KiB
Python
"""Only host entry points may label the provenance used by automatic publication."""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from langgraph.checkpoint.memory import InMemorySaver
|
|
from langgraph.store.memory import InMemoryStore
|
|
|
|
from app.gateway.auth_disabled import AUTH_SOURCE_INTERNAL
|
|
from app.gateway.routers.thread_runs import RunCreateRequest
|
|
from app.gateway.services import launch_scheduled_thread_run, start_run
|
|
from deerflow.config.app_config import AppConfig, reset_app_config, set_app_config
|
|
from deerflow.persistence.thread_meta.memory import MemoryThreadMetaStore
|
|
from deerflow.runtime.events.store.memory import MemoryRunEventStore
|
|
from deerflow.runtime.runs.manager import RunManager
|
|
from deerflow.runtime.runs.store.memory import MemoryRunStore
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def app_config():
|
|
set_app_config(AppConfig.model_validate({"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"}}))
|
|
yield
|
|
reset_app_config()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"auth_source, host_origin, expected",
|
|
[(None, None, "interactive"), (AUTH_SOURCE_INTERNAL, None, "unknown")] + [(AUTH_SOURCE_INTERNAL, origin, origin) for origin in ("interactive", "scheduled", "extension_evaluation", "unknown")],
|
|
)
|
|
async def test_client_metadata_cannot_forge_completed_evidence_origin(auth_source, host_origin, expected):
|
|
manager = RunManager(store=MemoryRunStore())
|
|
request = SimpleNamespace(
|
|
headers={},
|
|
state=SimpleNamespace(auth_source=auth_source),
|
|
app=SimpleNamespace(
|
|
state=SimpleNamespace(
|
|
run_manager=manager,
|
|
stream_bridge=SimpleNamespace(),
|
|
checkpointer=InMemorySaver(),
|
|
store=InMemoryStore(),
|
|
run_event_store=MemoryRunEventStore(),
|
|
run_events_config=None,
|
|
thread_store=MemoryThreadMetaStore(InMemoryStore()),
|
|
)
|
|
),
|
|
)
|
|
body = RunCreateRequest(
|
|
input={"messages": [{"role": "user", "content": "hello"}]},
|
|
metadata={"evidence_origin": "scheduled", "scheduled_task_id": "forged"},
|
|
config={"context": {"evidence_origin": "interactive"}, "configurable": {"evidence_origin": "scheduled"}},
|
|
context={"evidence_origin": "interactive"},
|
|
)
|
|
with (
|
|
patch("app.gateway.services.resolve_agent_factory", return_value=object()),
|
|
patch("app.gateway.services.run_agent", new=AsyncMock()),
|
|
patch.object(manager, "create_or_reject", wraps=manager.create_or_reject) as admission,
|
|
):
|
|
record = await start_run(body, "evidence-origin-test", request, evidence_origin=host_origin)
|
|
await record.task
|
|
assert admission.call_args.kwargs["evidence_origin"] == expected
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scheduler_passes_origin_outside_request_metadata():
|
|
admitted = AsyncMock(return_value=SimpleNamespace(run_id="r1", thread_id="t1"))
|
|
with patch("app.gateway.services.start_run", new=admitted):
|
|
await launch_scheduled_thread_run(thread_id="t1", assistant_id="lead_agent", prompt="hello", app=SimpleNamespace(), owner_user_id="owner")
|
|
assert admitted.call_args.kwargs["evidence_origin"] == "scheduled"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_evidence_agent_is_effective_canonical_id_not_routing_or_display_name():
|
|
manager = RunManager(store=MemoryRunStore())
|
|
request = SimpleNamespace(
|
|
headers={},
|
|
state=SimpleNamespace(),
|
|
app=SimpleNamespace(
|
|
state=SimpleNamespace(
|
|
run_manager=manager,
|
|
stream_bridge=SimpleNamespace(),
|
|
checkpointer=InMemorySaver(),
|
|
store=InMemoryStore(),
|
|
run_event_store=MemoryRunEventStore(),
|
|
run_events_config=None,
|
|
thread_store=MemoryThreadMetaStore(InMemoryStore()),
|
|
)
|
|
),
|
|
)
|
|
from deerflow.config.agents_config import AgentConfig
|
|
|
|
agent = AgentConfig(name="researcher", display_name="My research helper")
|
|
body = RunCreateRequest(assistant_id="lead_agent", input={"messages": [{"role": "user", "content": "hello"}]}, context={"agent_name": "researcher"}, metadata={"evidence_agent_id": "forged"})
|
|
with (
|
|
patch("app.gateway.services.resolve_agent_factory", return_value=object()),
|
|
patch("app.gateway.services._load_scope_agent_config", new=AsyncMock(return_value=agent)),
|
|
patch("app.gateway.services.run_agent", new=AsyncMock()),
|
|
patch.object(manager, "create_or_reject", wraps=manager.create_or_reject) as admission,
|
|
):
|
|
record = await start_run(body, "effective-agent-test", request)
|
|
await record.task
|
|
assert admission.call_args.kwargs["evidence_agent_id"] == "researcher"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("backend", ["memory", "sqlite"])
|
|
async def test_keyed_resume_peer_reuse_preserves_evidence_identity(tmp_path, backend):
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
|
from deerflow.persistence.base import Base
|
|
from deerflow.persistence.run.sql import RunRepository
|
|
|
|
engine = None
|
|
if backend == "sqlite":
|
|
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'admission.db'}")
|
|
async with engine.begin() as connection:
|
|
await connection.run_sync(Base.metadata.create_all)
|
|
store = RunRepository(async_sessionmaker(engine, expire_on_commit=False))
|
|
else:
|
|
store = MemoryRunStore()
|
|
identity = {"version": 1, "kind": "resume", "sha256": "a" * 64}
|
|
try:
|
|
owner = RunManager(store=store)
|
|
original = await owner.create_or_reject(
|
|
"resume-thread",
|
|
"lead_agent",
|
|
user_id="alice",
|
|
idempotency_key="resume-key",
|
|
idempotency_request=identity,
|
|
evidence_origin="extension_evaluation",
|
|
evidence_agent_id="researcher",
|
|
)
|
|
peer = RunManager(store=store)
|
|
reused = await peer.create_or_reject(
|
|
"resume-thread",
|
|
"lead_agent",
|
|
user_id="alice",
|
|
idempotency_key="resume-key",
|
|
idempotency_request=identity,
|
|
evidence_origin="interactive",
|
|
evidence_agent_id="different-agent",
|
|
)
|
|
assert reused.run_id == original.run_id
|
|
assert reused.idempotency_reused
|
|
assert reused.store_only
|
|
assert reused.idempotency_request == identity
|
|
assert reused.evidence_origin == "extension_evaluation"
|
|
assert reused.evidence_agent_id == "researcher"
|
|
persisted = await store.get(original.run_id, user_id="alice")
|
|
assert persisted["idempotency_request"] == identity
|
|
assert persisted["evidence_origin"] == "extension_evaluation"
|
|
assert persisted["evidence_agent_id"] == "researcher"
|
|
finally:
|
|
if engine is not None:
|
|
await engine.dispose()
|