diff --git a/backend/packages/harness/deerflow/runtime/runs/worker.py b/backend/packages/harness/deerflow/runtime/runs/worker.py index d40d69194..0dabf5a99 100644 --- a/backend/packages/harness/deerflow/runtime/runs/worker.py +++ b/backend/packages/harness/deerflow/runtime/runs/worker.py @@ -49,7 +49,7 @@ from deerflow.runtime.goal import ( ) from deerflow.runtime.serialization import serialize from deerflow.runtime.stream_bridge import StreamBridge -from deerflow.runtime.user_context import get_effective_user_id +from deerflow.runtime.user_context import resolve_runtime_user_id from deerflow.trace_context import DEERFLOW_TRACE_METADATA_KEY, get_current_trace_id, normalize_trace_id from deerflow.tracing import inject_langfuse_metadata from deerflow.utils.messages import message_to_text @@ -338,7 +338,7 @@ async def run_agent( inject_langfuse_metadata( config, thread_id=thread_id, - user_id=get_effective_user_id(), + user_id=resolve_runtime_user_id(runtime), assistant_id=record.assistant_id, model_name=record.model_name, environment=os.environ.get("DEER_FLOW_ENV") or os.environ.get("ENVIRONMENT"), diff --git a/backend/tests/test_worker_langfuse_metadata.py b/backend/tests/test_worker_langfuse_metadata.py index 558e09585..697debb1d 100644 --- a/backend/tests/test_worker_langfuse_metadata.py +++ b/backend/tests/test_worker_langfuse_metadata.py @@ -132,23 +132,79 @@ async def test_run_agent_injects_langfuse_metadata(monkeypatch): @pytest.mark.asyncio -async def test_run_agent_falls_back_to_default_user_when_unset(monkeypatch): - """When no user is in the contextvar, langfuse_user_id falls back to 'default'. +async def test_run_agent_uses_context_user_id_over_contextvar(monkeypatch): + """A run carrying ``context.user_id`` traces to that user, not the contextvar. - Uses ``monkeypatch.setattr`` to redirect ``get_effective_user_id`` to return - ``"default"`` rather than directly mutating the contextvar — direct contextvar - operations across pytest test boundaries have produced spooky cross-file - pollution when combined with the langfuse OTel global tracer provider. + Internal-token callers invoke a run on behalf of an end user, so the + ``_current_user`` ContextVar is never that end user. The caller instead + carries the real owner in the run request's ``config['context']['user_id']``, + which ``resolve_runtime_user_id(runtime)`` must prefer over the contextvar — + even though conftest's autouse fixture injects ``test-user-autouse`` into it. """ monkeypatch.setenv("LANGFUSE_TRACING", "true") monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-lf-test") monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-lf-test") from deerflow.config.tracing_config import reset_tracing_config - from deerflow.runtime.runs import worker as worker_module + + reset_tracing_config() + + fake_agent = _FakeAgent() + + def agent_factory(config): + return fake_agent + + record = RunRecord( + run_id="run-ctx-user", + thread_id="thread-ctx", + assistant_id="lead-agent", + status=RunStatus.pending, + on_disconnect=DisconnectMode.cancel, + ) + record.abort_event = asyncio.Event() + ctx = RunContext(checkpointer=None) + + await run_agent( + _FakeBridge(), + _FakeRunManager(), + record, + ctx=ctx, + agent_factory=agent_factory, + graph_input={"messages": []}, + config={ + "configurable": {"thread_id": "thread-ctx"}, + "context": {"user_id": "real-end-user"}, + }, + ) + + metadata = fake_agent.captured_config.get("metadata") or {} + # context.user_id wins over the contextvar's ``test-user-autouse``. + assert metadata.get("langfuse_user_id") == "real-end-user" + + +@pytest.mark.asyncio +async def test_run_agent_falls_back_to_default_user_when_unset(monkeypatch): + """When no user is in the contextvar (and no context.user_id), langfuse_user_id + falls back to 'default'. + + Uses ``monkeypatch.setattr`` to redirect ``get_effective_user_id`` to return + ``"default"`` rather than directly mutating the contextvar — direct contextvar + operations across pytest test boundaries have produced spooky cross-file + pollution when combined with the langfuse OTel global tracer provider. + + The worker resolves the trace user via ``resolve_runtime_user_id(runtime)``; + with no ``context.user_id`` it falls back to ``get_effective_user_id()`` — so + we patch that fallback at its definition module (``user_context``), which is + the name ``resolve_runtime_user_id`` actually calls. + """ + monkeypatch.setenv("LANGFUSE_TRACING", "true") + monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-lf-test") + monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-lf-test") + from deerflow.config.tracing_config import reset_tracing_config + from deerflow.runtime import user_context as user_context_module from deerflow.runtime.user_context import DEFAULT_USER_ID reset_tracing_config() - monkeypatch.setattr(worker_module, "get_effective_user_id", lambda: DEFAULT_USER_ID) + monkeypatch.setattr(user_context_module, "get_effective_user_id", lambda: DEFAULT_USER_ID) fake_agent = _FakeAgent()