deer-flow/backend/tests/test_completed_evidence_admission.py
Ryker_Feng d1d0958bea
feat(skills): add guarded evolution host APIs (#5645)
* 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>
2026-10-18 21:17:02 +08:00

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()