chore: import upstream snapshot with attribution
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,630 @@
|
||||
import json
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from chainlit.session import WebsocketSession
|
||||
from chainlit.socket import (
|
||||
_authenticate_connection,
|
||||
_get_token,
|
||||
_get_token_from_cookie,
|
||||
clean_session,
|
||||
connection_successful,
|
||||
load_user_env,
|
||||
persist_user_session,
|
||||
restore_existing_session,
|
||||
resume_thread,
|
||||
)
|
||||
|
||||
|
||||
class TestGetTokenFromCookie:
|
||||
"""Test suite for _get_token_from_cookie function."""
|
||||
|
||||
def test_get_token_from_cookie_with_valid_cookie(self):
|
||||
"""Test extracting token from valid cookie header."""
|
||||
with patch("chainlit.socket.get_token_from_cookies") as mock_get_token:
|
||||
mock_get_token.return_value = "test_token"
|
||||
environ = {"HTTP_COOKIE": "session=abc123; token=test_token"}
|
||||
|
||||
result = _get_token_from_cookie(environ)
|
||||
|
||||
assert result == "test_token"
|
||||
mock_get_token.assert_called_once()
|
||||
|
||||
def test_get_token_from_cookie_without_cookie(self):
|
||||
"""Test when no cookie header is present."""
|
||||
environ = {}
|
||||
result = _get_token_from_cookie(environ)
|
||||
assert result is None
|
||||
|
||||
def test_get_token_from_cookie_with_empty_cookie(self):
|
||||
"""Test with empty cookie header."""
|
||||
with patch("chainlit.socket.get_token_from_cookies") as mock_get_token:
|
||||
mock_get_token.return_value = None
|
||||
environ = {"HTTP_COOKIE": ""}
|
||||
|
||||
result = _get_token_from_cookie(environ)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestGetToken:
|
||||
"""Test suite for _get_token function."""
|
||||
|
||||
def test_get_token_calls_get_token_from_cookie(self):
|
||||
"""Test that _get_token delegates to _get_token_from_cookie."""
|
||||
with patch("chainlit.socket._get_token_from_cookie") as mock_get_cookie:
|
||||
mock_get_cookie.return_value = "token_value"
|
||||
environ = {"HTTP_COOKIE": "token=token_value"}
|
||||
|
||||
result = _get_token(environ)
|
||||
|
||||
assert result == "token_value"
|
||||
mock_get_cookie.assert_called_once_with(environ)
|
||||
|
||||
|
||||
class TestAuthenticateConnection:
|
||||
"""Test suite for _authenticate_connection function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_connection_with_valid_token(self):
|
||||
"""Test authentication with valid token."""
|
||||
mock_user = Mock()
|
||||
mock_user.identifier = "user123"
|
||||
|
||||
with patch("chainlit.socket._get_token") as mock_get_token:
|
||||
with patch("chainlit.socket.get_current_user") as mock_get_user:
|
||||
mock_get_token.return_value = "valid_token"
|
||||
mock_get_user.return_value = mock_user
|
||||
|
||||
environ = {"HTTP_COOKIE": "token=valid_token"}
|
||||
user, token = await _authenticate_connection(environ)
|
||||
|
||||
assert user == mock_user
|
||||
assert token == "valid_token"
|
||||
mock_get_user.assert_called_once_with(token="valid_token")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_connection_without_token(self):
|
||||
"""Test authentication without token."""
|
||||
with patch("chainlit.socket._get_token") as mock_get_token:
|
||||
mock_get_token.return_value = None
|
||||
|
||||
environ = {}
|
||||
user, token = await _authenticate_connection(environ)
|
||||
|
||||
assert user is None
|
||||
assert token is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_connection_with_invalid_token(self):
|
||||
"""Test authentication with invalid token."""
|
||||
with patch("chainlit.socket._get_token") as mock_get_token:
|
||||
with patch("chainlit.socket.get_current_user") as mock_get_user:
|
||||
mock_get_token.return_value = "invalid_token"
|
||||
mock_get_user.return_value = None
|
||||
|
||||
environ = {"HTTP_COOKIE": "token=invalid_token"}
|
||||
user, token = await _authenticate_connection(environ)
|
||||
|
||||
assert user is None
|
||||
assert token is None
|
||||
|
||||
|
||||
class TestRestoreExistingSession:
|
||||
"""Test suite for restore_existing_session function."""
|
||||
|
||||
def test_restore_existing_session_success(self):
|
||||
"""Test restoring an existing session."""
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.user = None
|
||||
emit_fn = Mock()
|
||||
emit_call_fn = Mock()
|
||||
environ = {"HTTP_COOKIE": "token=token"}
|
||||
|
||||
with patch.object(WebsocketSession, "get_by_id") as mock_get:
|
||||
mock_get.return_value = mock_session
|
||||
|
||||
result = restore_existing_session(
|
||||
"new_sid", "session_123", emit_fn, emit_call_fn, environ
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_session.restore.assert_called_once_with(new_socket_id="new_sid")
|
||||
assert mock_session.emit == emit_fn
|
||||
assert mock_session.emit_call == emit_call_fn
|
||||
assert mock_session.environ == environ
|
||||
|
||||
def test_restore_existing_session_with_matching_user(self):
|
||||
"""Test restoring a session for its authenticated owner."""
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.user = Mock(identifier="user123")
|
||||
authenticated_user = Mock(identifier="user123")
|
||||
|
||||
with patch.object(WebsocketSession, "get_by_id") as mock_get:
|
||||
mock_get.return_value = mock_session
|
||||
|
||||
result = restore_existing_session(
|
||||
"new_sid",
|
||||
"session_123",
|
||||
Mock(),
|
||||
Mock(),
|
||||
{"HTTP_COOKIE": "token=token"},
|
||||
user=authenticated_user,
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_session.restore.assert_called_once_with(new_socket_id="new_sid")
|
||||
|
||||
def test_restore_existing_session_rejects_different_user(self):
|
||||
"""Test that a session cannot be restored by another user."""
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.user = Mock(identifier="victim")
|
||||
authenticated_user = Mock(identifier="attacker")
|
||||
|
||||
with patch.object(WebsocketSession, "get_by_id") as mock_get:
|
||||
mock_get.return_value = mock_session
|
||||
|
||||
with pytest.raises(ConnectionRefusedError, match="authorization failed"):
|
||||
restore_existing_session(
|
||||
"new_sid",
|
||||
"session_123",
|
||||
Mock(),
|
||||
Mock(),
|
||||
{"HTTP_COOKIE": "token=token"},
|
||||
user=authenticated_user,
|
||||
)
|
||||
|
||||
mock_session.restore.assert_not_called()
|
||||
|
||||
def test_restore_existing_session_not_found(self):
|
||||
"""Test when session is not found."""
|
||||
with patch.object(WebsocketSession, "get_by_id") as mock_get:
|
||||
mock_get.return_value = None
|
||||
|
||||
result = restore_existing_session(
|
||||
"new_sid", "session_123", Mock(), Mock(), {"HTTP_COOKIE": "token=token"}
|
||||
)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestPersistUserSession:
|
||||
"""Test suite for persist_user_session function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persist_user_session_with_data_layer(self):
|
||||
"""Test persisting user session with data layer."""
|
||||
mock_data_layer = AsyncMock()
|
||||
|
||||
with patch("chainlit.socket.get_data_layer") as mock_get_dl:
|
||||
mock_get_dl.return_value = mock_data_layer
|
||||
|
||||
metadata = {"key": "value"}
|
||||
await persist_user_session("thread_123", metadata)
|
||||
|
||||
mock_data_layer.update_thread.assert_called_once_with(
|
||||
thread_id="thread_123", metadata=metadata
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persist_user_session_without_data_layer(self):
|
||||
"""Test persisting when no data layer is available."""
|
||||
with patch("chainlit.socket.get_data_layer") as mock_get_dl:
|
||||
mock_get_dl.return_value = None
|
||||
|
||||
# Should not raise an error
|
||||
await persist_user_session("thread_123", {"key": "value"})
|
||||
|
||||
|
||||
class TestResumeThread:
|
||||
"""Test suite for resume_thread function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_thread_without_data_layer(self):
|
||||
"""Test resume thread when no data layer exists."""
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.user = Mock()
|
||||
mock_session.thread_id_to_resume = "thread_123"
|
||||
|
||||
with patch("chainlit.socket.get_data_layer") as mock_get_dl:
|
||||
mock_get_dl.return_value = None
|
||||
|
||||
result = await resume_thread(mock_session)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_thread_without_user(self):
|
||||
"""Test resume thread when session has no user."""
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.user = None
|
||||
mock_session.thread_id_to_resume = "thread_123"
|
||||
|
||||
result = await resume_thread(mock_session)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_thread_without_thread_id(self):
|
||||
"""Test resume thread when no thread_id_to_resume."""
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.user = Mock()
|
||||
mock_session.thread_id_to_resume = None
|
||||
|
||||
result = await resume_thread(mock_session)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_thread_thread_not_found(self):
|
||||
"""Test resume thread when thread doesn't exist."""
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.user = Mock(identifier="user123")
|
||||
mock_session.thread_id_to_resume = "thread_123"
|
||||
mock_session.id = "session_123"
|
||||
|
||||
mock_data_layer = AsyncMock()
|
||||
mock_data_layer.get_thread.return_value = None
|
||||
|
||||
with patch("chainlit.socket.get_data_layer") as mock_get_dl:
|
||||
mock_get_dl.return_value = mock_data_layer
|
||||
|
||||
result = await resume_thread(mock_session)
|
||||
|
||||
assert result is None
|
||||
mock_data_layer.get_thread.assert_called_once_with(thread_id="thread_123")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_thread_user_not_author(self):
|
||||
"""Test resume thread when user is not the thread author."""
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.user = Mock(identifier="user123")
|
||||
mock_session.thread_id_to_resume = "thread_123"
|
||||
mock_session.id = "session_123"
|
||||
|
||||
thread = {"userIdentifier": "different_user", "metadata": {}}
|
||||
mock_data_layer = AsyncMock()
|
||||
mock_data_layer.get_thread.return_value = thread
|
||||
|
||||
with patch("chainlit.socket.get_data_layer") as mock_get_dl:
|
||||
mock_get_dl.return_value = mock_data_layer
|
||||
|
||||
result = await resume_thread(mock_session)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_thread_success(self):
|
||||
"""Test successful thread resumption."""
|
||||
from chainlit.user_session import user_sessions
|
||||
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.user = Mock(identifier="user123")
|
||||
mock_session.thread_id_to_resume = "thread_123"
|
||||
mock_session.id = "session_123"
|
||||
|
||||
metadata = {
|
||||
"chat_profile": "gpt-4",
|
||||
"chat_settings": {"temperature": 0.7},
|
||||
}
|
||||
thread = {"userIdentifier": "user123", "metadata": metadata}
|
||||
|
||||
mock_data_layer = AsyncMock()
|
||||
mock_data_layer.get_thread.return_value = thread
|
||||
|
||||
original_sessions = user_sessions.copy()
|
||||
try:
|
||||
with patch("chainlit.socket.get_data_layer") as mock_get_dl:
|
||||
mock_get_dl.return_value = mock_data_layer
|
||||
|
||||
result = await resume_thread(mock_session)
|
||||
|
||||
assert result == thread
|
||||
assert mock_session.chat_profile == "gpt-4"
|
||||
assert mock_session.chat_settings == {"temperature": 0.7}
|
||||
assert user_sessions.get("session_123") == metadata
|
||||
finally:
|
||||
user_sessions.clear()
|
||||
user_sessions.update(original_sessions)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_thread_with_string_metadata(self):
|
||||
"""Test thread resumption with JSON string metadata."""
|
||||
from chainlit.user_session import user_sessions
|
||||
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.user = Mock(identifier="user123")
|
||||
mock_session.thread_id_to_resume = "thread_123"
|
||||
mock_session.id = "session_123"
|
||||
|
||||
metadata_dict = {"chat_profile": "gpt-4"}
|
||||
thread = {
|
||||
"userIdentifier": "user123",
|
||||
"metadata": json.dumps(metadata_dict),
|
||||
}
|
||||
|
||||
mock_data_layer = AsyncMock()
|
||||
mock_data_layer.get_thread.return_value = thread
|
||||
|
||||
original_sessions = user_sessions.copy()
|
||||
try:
|
||||
with patch("chainlit.socket.get_data_layer") as mock_get_dl:
|
||||
mock_get_dl.return_value = mock_data_layer
|
||||
|
||||
result = await resume_thread(mock_session)
|
||||
|
||||
assert result == thread
|
||||
assert mock_session.chat_profile == "gpt-4"
|
||||
finally:
|
||||
user_sessions.clear()
|
||||
user_sessions.update(original_sessions)
|
||||
|
||||
|
||||
class TestLoadUserEnv:
|
||||
"""Test suite for load_user_env function."""
|
||||
|
||||
def test_load_user_env_with_valid_json(self):
|
||||
"""Test loading valid user environment JSON."""
|
||||
user_env = '{"API_KEY": "secret", "ENV_VAR": "value"}'
|
||||
|
||||
with patch("chainlit.socket.config") as mock_config:
|
||||
mock_config.project.user_env = []
|
||||
|
||||
result = load_user_env(user_env)
|
||||
|
||||
assert result == {"API_KEY": "secret", "ENV_VAR": "value"}
|
||||
|
||||
def test_load_user_env_with_required_keys(self):
|
||||
"""Test loading user env with required keys."""
|
||||
user_env = '{"API_KEY": "secret", "OTHER_KEY": "value"}'
|
||||
|
||||
with patch("chainlit.socket.config") as mock_config:
|
||||
mock_config.project.user_env = ["API_KEY", "OTHER_KEY"]
|
||||
|
||||
result = load_user_env(user_env)
|
||||
|
||||
assert result == {"API_KEY": "secret", "OTHER_KEY": "value"}
|
||||
|
||||
def test_load_user_env_missing_required_key(self):
|
||||
"""Test error when required key is missing."""
|
||||
user_env = '{"API_KEY": "secret"}'
|
||||
|
||||
with patch("chainlit.socket.config") as mock_config:
|
||||
mock_config.project.user_env = ["API_KEY", "MISSING_KEY"]
|
||||
|
||||
with pytest.raises(
|
||||
ConnectionRefusedError, match="Missing user environment variable"
|
||||
):
|
||||
load_user_env(user_env)
|
||||
|
||||
def test_load_user_env_none_with_required_keys(self):
|
||||
"""Test error when user_env is None but keys are required."""
|
||||
with patch("chainlit.socket.config") as mock_config:
|
||||
mock_config.project.user_env = ["API_KEY"]
|
||||
|
||||
with pytest.raises(
|
||||
ConnectionRefusedError, match="Missing user environment variables"
|
||||
):
|
||||
load_user_env(None)
|
||||
|
||||
def test_load_user_env_none_without_required_keys(self):
|
||||
"""Test when user_env is None and no keys are required."""
|
||||
with patch("chainlit.socket.config") as mock_config:
|
||||
mock_config.project.user_env = []
|
||||
|
||||
result = load_user_env(None)
|
||||
|
||||
assert result == {}
|
||||
|
||||
|
||||
class TestCleanSession:
|
||||
"""Test suite for clean_session function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clean_session_with_existing_session(self):
|
||||
"""Test marking session for cleanup."""
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.to_clear = False
|
||||
|
||||
with patch.object(WebsocketSession, "get") as mock_get:
|
||||
mock_get.return_value = mock_session
|
||||
|
||||
await clean_session("socket_123")
|
||||
|
||||
assert mock_session.to_clear is True
|
||||
mock_get.assert_called_once_with("socket_123")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clean_session_without_session(self):
|
||||
"""Test clean_session when session doesn't exist."""
|
||||
with patch.object(WebsocketSession, "get") as mock_get:
|
||||
mock_get.return_value = None
|
||||
|
||||
# Should not raise an error
|
||||
await clean_session("socket_123")
|
||||
|
||||
|
||||
class TestSocketEdgeCases:
|
||||
"""Test suite for socket edge cases."""
|
||||
|
||||
def test_restore_existing_session_with_none_session_id(self):
|
||||
"""Test restore with None session_id."""
|
||||
with patch.object(WebsocketSession, "get_by_id") as mock_get:
|
||||
mock_get.return_value = None
|
||||
|
||||
result = restore_existing_session(None, None, Mock(), Mock(), None)
|
||||
|
||||
assert result is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persist_user_session_with_empty_metadata(self):
|
||||
"""Test persisting empty metadata."""
|
||||
mock_data_layer = AsyncMock()
|
||||
|
||||
with patch("chainlit.socket.get_data_layer") as mock_get_dl:
|
||||
mock_get_dl.return_value = mock_data_layer
|
||||
|
||||
await persist_user_session("thread_123", {})
|
||||
|
||||
mock_data_layer.update_thread.assert_called_once_with(
|
||||
thread_id="thread_123", metadata={}
|
||||
)
|
||||
|
||||
def test_load_user_env_with_empty_json(self):
|
||||
"""Test loading empty user environment."""
|
||||
user_env = "{}"
|
||||
|
||||
with patch("chainlit.socket.config") as mock_config:
|
||||
mock_config.project.user_env = []
|
||||
|
||||
result = load_user_env(user_env)
|
||||
|
||||
assert result == {}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_thread_with_empty_metadata(self):
|
||||
"""Test resuming thread with empty metadata."""
|
||||
from chainlit.user_session import user_sessions
|
||||
|
||||
mock_session = Mock(spec=WebsocketSession)
|
||||
mock_session.user = Mock(identifier="user123")
|
||||
mock_session.thread_id_to_resume = "thread_123"
|
||||
mock_session.id = "session_123"
|
||||
|
||||
thread = {"userIdentifier": "user123", "metadata": {}}
|
||||
|
||||
mock_data_layer = AsyncMock()
|
||||
mock_data_layer.get_thread.return_value = thread
|
||||
|
||||
original_sessions = user_sessions.copy()
|
||||
try:
|
||||
with patch("chainlit.socket.get_data_layer") as mock_get_dl:
|
||||
mock_get_dl.return_value = mock_data_layer
|
||||
|
||||
result = await resume_thread(mock_session)
|
||||
|
||||
assert result == thread
|
||||
assert user_sessions.get("session_123") == {}
|
||||
finally:
|
||||
user_sessions.clear()
|
||||
user_sessions.update(original_sessions)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_connection_with_exception(self):
|
||||
"""Test authentication when get_current_user raises exception."""
|
||||
with patch("chainlit.socket._get_token") as mock_get_token:
|
||||
with patch("chainlit.socket.get_current_user") as mock_get_user:
|
||||
mock_get_token.return_value = "token"
|
||||
mock_get_user.side_effect = Exception("Auth error")
|
||||
|
||||
environ = {"HTTP_COOKIE": "token=token"}
|
||||
|
||||
# Should propagate the exception
|
||||
with pytest.raises(Exception, match="Auth error"):
|
||||
await _authenticate_connection(environ)
|
||||
|
||||
|
||||
class TestConnectionSuccessfulIdempotency:
|
||||
"""Regression tests: on_chat_start must fire exactly once per
|
||||
WebsocketSession, even when connection_successful is dispatched multiple
|
||||
times (Socket.IO reconnect, React StrictMode double-mount, reverse-proxy
|
||||
WS 101 retry).
|
||||
|
||||
Refs chainlit#2535, chainlit#2549, chainlit#2228.
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_chat_start_not_duplicated_on_reconnect(
|
||||
self, mock_session_factory
|
||||
):
|
||||
"""Reconnect path (session.restored=True): on_chat_start scheduled once."""
|
||||
on_chat_start = AsyncMock()
|
||||
|
||||
session = mock_session_factory(has_first_interaction=False)
|
||||
session.restored = True
|
||||
session.chat_started = False
|
||||
session.current_task = None
|
||||
session.thread_id_to_resume = None
|
||||
|
||||
mock_context = Mock()
|
||||
mock_context.session = session
|
||||
mock_context.emitter = AsyncMock()
|
||||
|
||||
mock_config = Mock()
|
||||
mock_config.code.on_chat_start = on_chat_start
|
||||
mock_config.code.on_chat_resume = None
|
||||
|
||||
with (
|
||||
patch("chainlit.socket.init_ws_context", return_value=mock_context),
|
||||
patch("chainlit.socket.config", mock_config),
|
||||
):
|
||||
await connection_successful("sid-1")
|
||||
# Simulate reconnect: same session object, chat_started now True.
|
||||
await connection_successful("sid-1")
|
||||
|
||||
assert on_chat_start.call_count == 1, (
|
||||
"on_chat_start must be scheduled exactly once per WebsocketSession"
|
||||
)
|
||||
assert session.chat_started is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_chat_start_fires_once_on_fresh_session(
|
||||
self, mock_session_factory
|
||||
):
|
||||
"""Normal one-connect path still greets exactly once."""
|
||||
on_chat_start = AsyncMock()
|
||||
|
||||
session = mock_session_factory(has_first_interaction=False)
|
||||
session.restored = False
|
||||
session.chat_started = False
|
||||
session.current_task = None
|
||||
session.thread_id_to_resume = None
|
||||
|
||||
mock_context = Mock()
|
||||
mock_context.session = session
|
||||
mock_context.emitter = AsyncMock()
|
||||
|
||||
mock_config = Mock()
|
||||
mock_config.code.on_chat_start = on_chat_start
|
||||
mock_config.code.on_chat_resume = None
|
||||
|
||||
with (
|
||||
patch("chainlit.socket.init_ws_context", return_value=mock_context),
|
||||
patch("chainlit.socket.config", mock_config),
|
||||
):
|
||||
await connection_successful("sid-1")
|
||||
|
||||
assert on_chat_start.call_count == 1
|
||||
assert session.chat_started is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_chat_start_not_duplicated_on_fresh_then_reconnect(
|
||||
self, mock_session_factory
|
||||
):
|
||||
"""Fresh connect followed by a reconnect still fires exactly once."""
|
||||
on_chat_start = AsyncMock()
|
||||
|
||||
session = mock_session_factory(has_first_interaction=False)
|
||||
session.restored = False
|
||||
session.chat_started = False
|
||||
session.current_task = None
|
||||
session.thread_id_to_resume = None
|
||||
|
||||
mock_context = Mock()
|
||||
mock_context.session = session
|
||||
mock_context.emitter = AsyncMock()
|
||||
|
||||
mock_config = Mock()
|
||||
mock_config.code.on_chat_start = on_chat_start
|
||||
mock_config.code.on_chat_resume = None
|
||||
|
||||
with (
|
||||
patch("chainlit.socket.init_ws_context", return_value=mock_context),
|
||||
patch("chainlit.socket.config", mock_config),
|
||||
):
|
||||
await connection_successful("sid-1")
|
||||
session.restored = True
|
||||
await connection_successful("sid-1")
|
||||
|
||||
assert on_chat_start.call_count == 1
|
||||
Reference in New Issue
Block a user