mirror of
https://github.com/langgenius/dify.git
synced 2026-06-05 15:40:14 +08:00
fix(api): dedup EndUser in plugin get_user by session_id for Reverse Invocation (#36742)
This commit is contained in:
@@ -45,6 +45,15 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser:
|
||||
.limit(1)
|
||||
)
|
||||
else:
|
||||
# Try id first (preserves the original "explicit end-user
|
||||
# id → that specific user" semantics for callers that pass
|
||||
# a known EndUser.id). Fall back to session_id so daemon-
|
||||
# supplied session UUIDs dedup against the row created on
|
||||
# the first Reverse Invocation call — without this, an
|
||||
# id-only lookup never matched (create writes user_id to
|
||||
# session_id, id is auto-generated) and a fresh EndUser
|
||||
# was created per call, breaking multi-turn chat
|
||||
# continuation (see #36736).
|
||||
user_model = session.scalar(
|
||||
select(EndUser)
|
||||
.where(
|
||||
@@ -53,6 +62,15 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser:
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
if user_model is None:
|
||||
user_model = session.scalar(
|
||||
select(EndUser)
|
||||
.where(
|
||||
EndUser.session_id == user_id,
|
||||
EndUser.tenant_id == tenant_id,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if not user_model:
|
||||
user_model = EndUser(
|
||||
|
||||
@@ -94,9 +94,39 @@ class TestGetUser:
|
||||
# Assert
|
||||
assert result == mock_new_user
|
||||
mock_session.get.assert_not_called()
|
||||
mock_session.scalar.assert_called_once()
|
||||
# Non-anonymous miss now tries id, then session_id fallback (see
|
||||
# #36736); both miss in this tenant → fall through to create.
|
||||
assert mock_session.scalar.call_count == 2
|
||||
mock_session.add.assert_called_once_with(mock_new_user)
|
||||
|
||||
@patch("controllers.inner_api.plugin.wraps.select")
|
||||
@patch("controllers.inner_api.plugin.wraps.EndUser")
|
||||
@patch("controllers.inner_api.plugin.wraps.sessionmaker")
|
||||
@patch("controllers.inner_api.plugin.wraps.db")
|
||||
def test_should_return_existing_user_by_session_id_fallback_for_non_anonymous(
|
||||
self, mock_db, mock_sessionmaker, mock_enduser_class, mock_select, app: Flask
|
||||
):
|
||||
"""Non-anonymous user_id misses on EndUser.id but hits on
|
||||
EndUser.session_id — this is the plugin-daemon Reverse Invocation
|
||||
case where the daemon sends a stable session-derived UUID that
|
||||
was written into session_id on the first call. See #36736.
|
||||
"""
|
||||
# Arrange
|
||||
mock_user = MagicMock()
|
||||
mock_session = MagicMock()
|
||||
mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session
|
||||
# First scalar (id lookup) returns None, second (session_id fallback) hits.
|
||||
mock_session.scalar.side_effect = [None, mock_user]
|
||||
|
||||
# Act
|
||||
with app.app_context():
|
||||
result = get_user("tenant123", "daemon-session-uuid")
|
||||
|
||||
# Assert
|
||||
assert result == mock_user
|
||||
assert mock_session.scalar.call_count == 2
|
||||
mock_session.add.assert_not_called()
|
||||
|
||||
@patch("controllers.inner_api.plugin.wraps.select")
|
||||
@patch("controllers.inner_api.plugin.wraps.EndUser")
|
||||
@patch("controllers.inner_api.plugin.wraps.sessionmaker")
|
||||
|
||||
Reference in New Issue
Block a user