deer-flow/backend/tests/test_run_events_endpoint.py
ShitK b1984cf4ab
fix(security): reject legacy MCP credentials in run metadata (#4448)
* docs: design run metadata secret admission

* docs: refine run metadata secret boundaries

* docs: plan run metadata secret fix

* fix(security): centralize legacy run metadata policy

* fix(security): reject secrets at run admission

* fix(security): hide legacy secrets from history APIs

* docs(security): migrate MCP credentials to secret context

* fix(security): redact legacy runnable config metadata

* fix(security): reject legacy config metadata credentials

* fix(security): hide legacy secrets from run kwargs

* docs(security): clarify config redaction boundary

* docs: keep issue 4416 planning local
2026-07-28 21:31:23 +08:00

146 lines
4.5 KiB
Python

"""The /events route forwards task_id + after_seq to the store (#3779).
The subtask card pages through one subagent task's persisted steps via these
query params; this locks the wiring so a rename/typo can't silently drop them
(which would make reload backfill fetch the whole run again, or nothing).
"""
import hashlib
from types import SimpleNamespace
from unittest import mock
import pytest
from langchain_core.messages import HumanMessage
from deerflow.agents.middlewares.dynamic_context_middleware import DynamicContextMiddleware
from deerflow.runtime.events.store.memory import MemoryRunEventStore
from deerflow.runtime.journal import RunJournal
@pytest.mark.anyio
async def test_list_run_events_forwards_task_id_and_after_seq():
from app.gateway.routers.thread_runs import list_run_events
calls: dict = {}
class FakeStore:
async def list_events(self, thread_id, run_id, *, event_types=None, task_id=None, limit=500, after_seq=None):
calls.update(thread_id=thread_id, run_id=run_id, event_types=event_types, task_id=task_id, limit=limit, after_seq=after_seq)
return [{"seq": 1, "event_type": "subagent.step"}]
class FakeState:
run_event_store = FakeStore()
class FakeApp:
state = FakeState()
class FakeRequest:
app = FakeApp()
_deerflow_test_bypass_auth = True
result = await list_run_events(
thread_id="t1",
run_id="r1",
request=FakeRequest(),
event_types="subagent.start,subagent.step,subagent.end",
task_id="task-A",
limit=500,
after_seq=7,
)
assert result == [{"seq": 1, "event_type": "subagent.step"}]
assert calls["task_id"] == "task-A"
assert calls["after_seq"] == 7
assert calls["event_types"] == ["subagent.start", "subagent.step", "subagent.end"]
@pytest.mark.anyio
async def test_list_run_events_redacts_historical_run_start_metadata():
from app.gateway.routers.thread_runs import list_run_events
stored_row = {
"seq": 1,
"event_type": "run.start",
"metadata": {
"caller": "lead_agent",
"auth_token": "legacy-secret",
"token_usage": 7,
},
}
class FakeStore:
async def list_events(self, thread_id, run_id, *, event_types=None, task_id=None, limit=500, after_seq=None):
return [stored_row]
class FakeState:
run_event_store = FakeStore()
class FakeApp:
state = FakeState()
class FakeRequest:
app = FakeApp()
_deerflow_test_bypass_auth = True
events = await list_run_events(
thread_id="legacy-thread",
run_id="legacy-run",
request=FakeRequest(),
event_types=None,
task_id=None,
limit=500,
after_seq=None,
)
assert events[0]["metadata"] == {
"caller": "lead_agent",
"token_usage": 7,
}
assert stored_row["metadata"]["auth_token"] == "legacy-secret"
assert events[0] is not stored_row
@pytest.mark.anyio
async def test_effective_memory_flows_from_injection_to_the_existing_debug_api():
"""The production run-events route is the field-level consumer for M1."""
from app.gateway.routers.thread_runs import list_run_events
store = MemoryRunEventStore()
journal = RunJournal("r1", "t1", store, flush_threshold=100)
runtime = SimpleNamespace(context={"__run_journal": journal})
memory = "<memory>\nUser prefers Python.\n</memory>\n"
with (
mock.patch("deerflow.agents.lead_agent.prompt._get_memory_context", return_value=memory),
mock.patch("deerflow.agents.middlewares.dynamic_context_middleware.datetime") as mock_dt,
):
mock_dt.now.return_value.strftime.return_value = "2026-05-08, Friday"
update = DynamicContextMiddleware().before_agent(
{"messages": [HumanMessage(content="Hi", id="msg-1")]},
runtime,
)
await journal.flush()
class FakeState:
run_event_store = store
class FakeApp:
state = FakeState()
class FakeRequest:
app = FakeApp()
_deerflow_test_bypass_auth = True
events = await list_run_events(
thread_id="t1",
run_id="r1",
request=FakeRequest(),
event_types="context:memory",
task_id=None,
limit=500,
after_seq=None,
)
effective_content = update["messages"][1].content
assert events[0]["content"] == {"content_sha256": hashlib.sha256(effective_content.encode("utf-8")).hexdigest()}