Spaces:
Running
Running
import pytest | |
import sys # Import sys for sys.modules patching | |
from unittest.mock import patch, AsyncMock, MagicMock | |
import app # Import app at the module level | |
# Assuming app.py will have on_chat_start and InsightFlowState | |
# from app import InsightFlowState # No longer needed here if app is imported above | |
# We will create on_chat_start in app.py next | |
# Define MockUserMessage at the module level | |
class MockUserMessage: | |
def __init__(self, content): | |
self.content = content | |
self.author = "user" # Add other attrs if on_message checks them | |
async def test_on_chat_start_initializes_state_and_sends_welcome(mock_cl): | |
""" | |
Test that on_chat_start initializes InsightFlowState, stores it, | |
and sends a welcome message. | |
""" | |
# with patch.dict(sys.modules, {'chainlit': mock_cl}): # Old way | |
# from app import InsightFlowState, on_chat_start # Old way | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
# mock_cl_in_app is now app.cl for the duration of this 'with' block | |
# mock_cl (the fixture) has pre-configured .Message, .user_session etc. | |
# Configure the mock_cl_in_app fixture's return values if needed before the call | |
# The mock_cl fixture already configures .Message.return_value.send to be an AsyncMock | |
# So, mock_cl_in_app.Message.return_value.send will be an AsyncMock. | |
# If we need to reset call counts for this specific test because mock_cl might be shared or stateful (it is function-scoped though): | |
mock_cl_in_app.Message.reset_mock() # Reset calls to the class | |
if hasattr(mock_cl_in_app.Message.return_value, 'send') and hasattr(mock_cl_in_app.Message.return_value.send, 'reset_mock'): | |
mock_cl_in_app.Message.return_value.send.reset_mock() # Reset calls to the send method of the instance returned by Message() | |
mock_cl_in_app.user_session.set.reset_mock() | |
await app.on_chat_start() # Call app.on_chat_start directly | |
# --- Assertions for messages --- | |
# 1. Check that cl.ChatSettings() was constructed and its send method was called | |
mock_cl_in_app.ChatSettings.assert_called_once() | |
# mock_cl_in_app.ChatSettings.return_value.send.assert_called_once() # send is called on the instance | |
# 2. Check that cl.Message(content="Welcome...") was constructed and its send method was called | |
found_welcome_message_constructor_call = False | |
for call_args_obj in mock_cl_in_app.Message.call_args_list: | |
args, kwargs = call_args_obj | |
content = kwargs.get("content") | |
if content == "Welcome to InsightFlow AI! Adjust settings using the gear icon ⚙️.": | |
found_welcome_message_constructor_call = True | |
# To check if ITS send method was called, you'd need to ensure Message() returns distinct mocks | |
# or check the send mock that is shared if Message.return_value is always the same mock instance. | |
# For now, let's assume the fixture mock_cl.Message.return_value.send covers it. | |
break | |
assert found_welcome_message_constructor_call, "cl.Message(content='Welcome...') constructor call not found" | |
# Ensure the generic send method on the instance returned by cl.Message() was called for the welcome message. | |
# This depends on how mock_cl.Message is set up. If it always returns the same instance, | |
# then mock_cl_in_app.Message.return_value.send.call_count would reflect sends from ChatSettings AND Message. | |
# The fixture mock_cl.Message.return_value.send = AsyncMock() is a single AsyncMock. | |
# We expect at least two 'send' operations: one from ChatSettings, one from Message. | |
# The ChatSettings().send() is already asserted by mock_cl_in_app.ChatSettings.return_value.send.assert_called_once() implicitly by fixture setup. | |
# The cl.Message().send() for welcome message is what we check below. | |
# Check that send was called on the instance returned by Message() at least once for the welcome message | |
# This is a bit tricky because ChatSettings also sends. | |
# Let's count total calls to the shared send mock: | |
assert mock_cl_in_app.Message.return_value.send.call_count >= 1 # At least the welcome message's send | |
# --- Previous assertions for state and welcome message --- (Simplified above) | |
# Count set calls, find 'insight_flow_state', verify its content (as before) | |
# Assert Welcome message: mock_cl_in_app.Message.assert_any_call(content="Welcome to InsightFlow AI!") | |
# This test previously asserted mock_cl_in_app.Message.assert_called_once_with(...) for welcome. | |
# If we add another message with an Action, we need to use assert_any_call or check call_args_list. | |
# Remove old assertions for "Configure your research team:" message and cl.Action calls for persona selection, | |
# as these are now handled by ChatSettings. | |
# found_welcome_message = False | |
# found_persona_action_message_text = False # Flag for the message text itself | |
# action_in_message = None # To store the action object if found | |
# # print(f"DEBUG: mock_cl_in_app.Message.call_args_list = {mock_cl_in_app.Message.call_args_list}") # Remove DEBUG PRINT | |
# for call_args_obj in mock_cl_in_app.Message.call_args_list: | |
# args, kwargs = call_args_obj | |
# content = kwargs.get("content") | |
# actions = kwargs.get("actions") | |
# if content == "Welcome to InsightFlow AI!": # Old welcome text | |
# found_welcome_message = True | |
# # This part is removed as ChatSettings handles persona UI now | |
# # if content == "Configure your research team:": | |
# # found_persona_action_message_text = True | |
# # if actions and isinstance(actions, list) and len(actions) == 1: | |
# # action_in_message = actions[0] | |
# assert found_welcome_message, "Welcome message not found" # Replaced by found_welcome_message_constructor_call | |
# # assert found_persona_action_message_text, "Message text 'Configure your research team:' not found" # REMOVED | |
# # assert action_in_message is not None, "No action found in the 'Configure your research team:' message" # REMOVED | |
# # Assert that cl.Action (the class mock) was called correctly - REMOVED, ChatSettings handles this | |
# # mock_cl_in_app.Action.assert_called_once_with( | |
# # name="select_personas", | |
# # label="Select/Update Personas", | |
# # description="Choose which personas to engage for the analysis.", | |
# # payload={"value": "trigger_selection"} | |
# # ) | |
# Assert that the send method was called for these messages | |
# mock_cl_in_app.Message.return_value.send should have been called at least twice -- reduced to >=1 for welcome msg specifically | |
# assert mock_cl_in_app.Message.return_value.send.call_count >= 2 # Reduced | |
# Verify insight_flow_state initialization (keeping original assertions) | |
assert mock_cl_in_app.user_session.set.call_count >= 5 # Initial state, 4 UI toggles (direct, quick, show_persp, show_viz), persona_factory. | |
# progress_msg is set to None, so total 7. Let's keep it at >=5 for now. | |
insight_state_call = None | |
for call_args_obj in mock_cl_in_app.user_session.set.call_args_list: | |
if call_args_obj[0][0] == 'insight_flow_state': | |
insight_state_call = call_args_obj | |
break | |
assert insight_state_call is not None, "'insight_flow_state' was not set in user_session" | |
saved_state = insight_state_call[0][1] | |
assert isinstance(saved_state, dict) | |
assert saved_state.get("current_step_name") == "awaiting_query" | |
async def test_on_message_direct_on_command(mock_cl): | |
"""Test that on_message correctly handles the '/direct on' command.""" | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
# Simulate user message | |
# user_message_mock = mock_cl.Message(content="/direct on") # Old line, mock_cl is the fixture, not the patched app.cl | |
# # user_message_mock.author = "user" | |
# # Mock the send method for the confirmation message | |
confirmation_message_instance_mock = AsyncMock() | |
confirmation_message_instance_mock.send = AsyncMock() | |
# Configure the patched app.cl's Message attribute for this test | |
# mock_cl_in_app is the MagicMock instance (our fixture mock_cl) that now replaces app.cl | |
mock_cl_in_app.Message.return_value = confirmation_message_instance_mock | |
# Ensure any previous call counts on the fixture's Message attribute are reset if necessary, | |
# though patching with a fresh `new=mock_cl` for each test should handle this. | |
# mock_cl_in_app.Message.reset_mock() # If mock_cl was reused across tests without @patch re-instantiating it. | |
# But here, mock_cl is function-scoped, and patch.object uses it freshly. | |
user_message_obj = MockUserMessage(content="/direct on") | |
await app.on_message(user_message_obj) # Call app.on_message directly | |
# Check that cl.user_session.set was called correctly on the patched app.cl | |
mock_cl_in_app.user_session.set.assert_any_call("direct_mode", True) | |
# Check that a confirmation message was sent using the patched app.cl | |
mock_cl_in_app.Message.assert_called_with(content="Direct mode ENABLED.") | |
confirmation_message_instance_mock.send.assert_called_once() | |
async def test_on_message_direct_off_command(mock_cl): | |
"""Test that on_message correctly handles the '/direct off' command.""" | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
user_message_obj = MockUserMessage(content="/direct off") | |
confirmation_message_instance_mock = AsyncMock(send=AsyncMock()) | |
mock_cl_in_app.Message.return_value = confirmation_message_instance_mock | |
await app.on_message(user_message_obj) | |
mock_cl_in_app.user_session.set.assert_any_call("direct_mode", False) | |
mock_cl_in_app.Message.assert_called_with(content="Direct mode DISABLED.") | |
confirmation_message_instance_mock.send.assert_called_once() | |
async def test_on_message_show_perspectives_on_command(mock_cl): | |
"""Test '/show perspectives on' command.""" | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
user_message_obj = MockUserMessage(content="/show perspectives on") | |
confirmation_msg_mock = AsyncMock(send=AsyncMock()) | |
mock_cl_in_app.Message.return_value = confirmation_msg_mock | |
await app.on_message(user_message_obj) | |
mock_cl_in_app.user_session.set.assert_any_call("show_perspectives", True) | |
mock_cl_in_app.Message.assert_called_with(content="Show perspectives ENABLED.") | |
confirmation_msg_mock.send.assert_called_once() | |
async def test_on_message_show_perspectives_off_command(mock_cl): | |
"""Test '/show perspectives off' command.""" | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
user_message_obj = MockUserMessage(content="/show perspectives off") | |
confirmation_msg_mock = AsyncMock(send=AsyncMock()) | |
mock_cl_in_app.Message.return_value = confirmation_msg_mock | |
await app.on_message(user_message_obj) | |
mock_cl_in_app.user_session.set.assert_any_call("show_perspectives", False) | |
mock_cl_in_app.Message.assert_called_with(content="Show perspectives DISABLED.") | |
confirmation_msg_mock.send.assert_called_once() | |
async def test_on_message_show_visualization_on_command(mock_cl): | |
"""Test '/show visualization on' command.""" | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
user_message_obj = MockUserMessage(content="/show visualization on") | |
confirmation_msg_mock = AsyncMock(send=AsyncMock()) | |
mock_cl_in_app.Message.return_value = confirmation_msg_mock | |
await app.on_message(user_message_obj) | |
mock_cl_in_app.user_session.set.assert_any_call("show_visualization", True) | |
mock_cl_in_app.Message.assert_called_with(content="Show visualization ENABLED.") | |
confirmation_msg_mock.send.assert_called_once() | |
async def test_on_message_show_visualization_off_command(mock_cl): | |
"""Test '/show visualization off' command.""" | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
user_message_obj = MockUserMessage(content="/show visualization off") | |
confirmation_msg_mock = AsyncMock(send=AsyncMock()) | |
mock_cl_in_app.Message.return_value = confirmation_msg_mock | |
await app.on_message(user_message_obj) | |
mock_cl_in_app.user_session.set.assert_any_call("show_visualization", False) | |
mock_cl_in_app.Message.assert_called_with(content="Show visualization DISABLED.") | |
confirmation_msg_mock.send.assert_called_once() | |
async def test_on_message_quick_mode_on_command(mock_cl): | |
"""Test that on_message correctly handles the '/quick_mode on' command.""" | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
user_message_obj = MockUserMessage(content="/quick_mode on") | |
confirmation_msg_mock = AsyncMock(send=AsyncMock()) | |
mock_cl_in_app.Message.return_value = confirmation_msg_mock | |
await app.on_message(user_message_obj) | |
mock_cl_in_app.user_session.set.assert_any_call("quick_mode", True) | |
mock_cl_in_app.Message.assert_called_with(content="Quick mode ENABLED.") | |
confirmation_msg_mock.send.assert_called_once() | |
async def test_on_message_quick_mode_off_command(mock_cl): | |
"""Test that on_message correctly handles the '/quick_mode off' command.""" | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
user_message_obj = MockUserMessage(content="/quick_mode off") | |
confirmation_msg_mock = AsyncMock(send=AsyncMock()) | |
mock_cl_in_app.Message.return_value = confirmation_msg_mock | |
await app.on_message(user_message_obj) | |
mock_cl_in_app.user_session.set.assert_any_call("quick_mode", False) | |
mock_cl_in_app.Message.assert_called_with(content="Quick mode DISABLED.") | |
confirmation_msg_mock.send.assert_called_once() | |
async def test_on_select_personas_action_sends_selection_ui(mock_cl): | |
"""Test that clicking the 'select_personas' action sends a message with persona selection UI.""" | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
# 1. Mock PersonaFactory and its get_available_personas method | |
mock_persona_factory_instance = AsyncMock() | |
available_personas_data = [ | |
{"id": "analytical", "name": "Analytical Abe", "description": "Focuses on data.", "default_selected": True}, | |
{"id": "scientific", "name": "Scientific Sue", "description": "Uses scientific method.", "default_selected": False}, | |
{"id": "philosophical", "name": "Philosophical Phil", "description": "Explores meaning.", "default_selected": True} | |
] | |
mock_persona_factory_instance.get_available_personas = MagicMock(return_value=available_personas_data) | |
# Configure cl.user_session.get to return our mock factory | |
original_get = mock_cl_in_app.user_session.get | |
def mock_user_session_get(key, default=None): | |
if key == "persona_factory": | |
return mock_persona_factory_instance | |
# For 'insight_flow_state', it should return the initial state which contains selected_personas | |
elif key == "insight_flow_state": | |
return {"selected_personas": [p["id"] for p in available_personas_data if p["default_selected"]]} | |
return original_get(key, default) | |
mock_cl_in_app.user_session.get = MagicMock(side_effect=mock_user_session_get) | |
# Mock cl.Select and cl.Action as they will be instantiated by the app | |
# mock_cl_in_app.Select is already mocked by mock_cl fixture if it exists there, or we ensure it here | |
mock_cl_in_app.Select = MagicMock() | |
mock_cl_in_app.Action = MagicMock() # Mock for the 'Update Personas' action | |
# 2. Simulate the action callback | |
# The action passed to the callback has name and value (payload) | |
action_payload_value = "trigger_selection" # Matches what on_chat_start sets | |
mock_action_instance = MagicMock(name="select_personas", payload={"value": action_payload_value}) | |
# Ensure the select_personas_action function exists in app.py and is decorated | |
# We will create this function in app.py next. | |
# For now, let's assume it's called like: await app.select_personas_action(mock_action_instance) | |
# If the function is decorated, Chainlit calls it. We need to find a way to trigger it | |
# or test its internal logic if direct invocation is hard. | |
# Assuming chainlit calls it, we need to get a reference to the decorated function. | |
# Let's assume we can find the decorated function via app module or by patching how Chainlit finds it. | |
# For now, we'll patch app.select_personas_action IF it's not easily discoverable how Chainlit invokes it. | |
# Simpler: Call it directly if it's a standalone async def in app.py decorated with @cl.action_callback | |
# The decorator just registers it. We can call the function itself. | |
if not hasattr(app, 'select_personas_action'): | |
pytest.skip("app.select_personas_action not yet implemented") | |
await app.select_personas_action(mock_action_instance) | |
# 3. Assertions | |
# Assert that get_available_personas was called | |
mock_persona_factory_instance.get_available_personas.assert_called_once() | |
# Assert that cl.Select was called for each persona | |
assert mock_cl_in_app.Select.call_count == len(available_personas_data) | |
for persona_data in available_personas_data: | |
initial_selected_ids = [p["id"] for p in available_personas_data if p["default_selected"]] | |
mock_cl_in_app.Select.assert_any_call( | |
id=persona_data["id"], | |
label=persona_data["name"], | |
initial_value=persona_data["id"] in initial_selected_ids # Check against current state | |
) | |
# Assert that cl.Action was called for the 'Update Individual Personas' button | |
mock_cl_in_app.Action.assert_any_call( | |
name="submit_persona_selection", | |
label="Update Individual Personas", | |
payload={} | |
) | |
# Assert that cl.Action was called for each team | |
# Need to ensure app.PERSONA_TEAMS is available in the test context or mock it. | |
# For simplicity, let's assume app.PERSONA_TEAMS is imported and accessible. | |
# We might need to `import app` and use `app.PERSONA_TEAMS`. | |
# Or, if app.py is patched, ensure PERSONA_TEAMS is part of the patch or globally available. | |
# The `with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app:` block is active. | |
# `app.PERSONA_TEAMS` should be accessible directly if `app` is imported in the test file. | |
assert len(app.PERSONA_TEAMS) > 0, "app.PERSONA_TEAMS should be populated for this test part" | |
for team_id, team_info in app.PERSONA_TEAMS.items(): | |
mock_cl_in_app.Action.assert_any_call( | |
name="handle_team_selection", | |
label=team_info["name"], | |
description=team_info["description"], | |
payload={"team_id": team_id} | |
) | |
# Assert that cl.Message was called to send the selects and the actions | |
message_with_selects_and_actions_found = False | |
for call_args_item in mock_cl_in_app.Message.call_args_list: | |
args, kwargs = call_args_item | |
elements = kwargs.get("elements", []) | |
actions = kwargs.get("actions", []) | |
# Expected actions: num_teams + 1 (for Update Individual Personas) | |
if len(elements) == len(available_personas_data) and len(actions) == (len(app.PERSONA_TEAMS) + 1): | |
all_elements_are_selects = all(isinstance(el, type(mock_cl_in_app.Select.return_value)) for el in elements) | |
# Check if at least one action is for 'handle_team_selection' and one for 'submit_persona_selection' | |
has_team_action = any(ac.name == "handle_team_selection" for ac in mock_cl_in_app.Action.call_args_list if ac.name == "handle_team_selection") | |
has_update_action = any(ac.name == "submit_persona_selection" for ac in mock_cl_in_app.Action.call_args_list if ac.name == "submit_persona_selection") | |
# This check on action instances in kwargs.get("actions") is tricky with MagicMock. | |
# It's better to rely on mock_cl_in_app.Action.assert_any_call for specific action creations. | |
# The check `len(actions) == (len(app.PERSONA_TEAMS) + 1)` is a good structural check for the message. | |
if all_elements_are_selects: | |
message_with_selects_and_actions_found = True | |
break | |
assert message_with_selects_and_actions_found, "Message with persona select UI and all actions not sent correctly" | |
# Assert that the message was sent | |
mock_cl_in_app.Message.return_value.send.assert_called() # Called at least once for this message | |
async def test_on_submit_persona_selection_action_updates_state(mock_cl): | |
"""Test that submitting persona selections updates the state and sends confirmation.""" | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
# 1. Initial state setup | |
initial_selected_personas = ["analytical"] | |
mock_initial_state = { | |
"query": "test query", | |
"selected_personas": initial_selected_personas, | |
# other fields as necessary for InsightFlowState | |
} | |
# Store the calls to set to verify later | |
user_session_set_calls = [] | |
def mock_user_session_set_side_effect(key, value): | |
user_session_set_calls.append((key, value)) | |
# Actual set on the mock session if needed for subsequent gets within the same callback | |
mock_cl_in_app.user_session._actual_session_data[key] = value | |
mock_cl_in_app.user_session._actual_session_data = {"insight_flow_state": mock_initial_state.copy()} | |
mock_cl_in_app.user_session.get = MagicMock( | |
side_effect=lambda key, default=None: mock_cl_in_app.user_session._actual_session_data.get(key, default) | |
) | |
mock_cl_in_app.user_session.set = MagicMock(side_effect=mock_user_session_set_side_effect) | |
# 2. Simulate the submitted values from cl.Select elements | |
# These are passed by Chainlit as the `values` argument to the callback | |
submitted_persona_values = { | |
"analytical": False, # User unselected analytical | |
"scientific": True, # User selected scientific | |
"philosophical": True # User selected philosophical | |
# Assume these IDs match cl.Select IDs from previous step | |
} | |
# 3. Mock the action instance that would be passed (though its attributes might not be crucial if `values` is directly passed) | |
mock_submit_action = MagicMock(name="submit_persona_selection") | |
# 4. Call the action callback function (to be created in app.py) | |
# The callback signature will be something like: async def submit_persona_selection_action(action: cl.Action, values: Dict[str, bool]) | |
# However, Chainlit injects `values` automatically. The test needs to simulate this. | |
# Let's assume the app function is `app.submit_persona_selection_action(action, values)` for testing. | |
# In reality, it might be `app.submit_persona_selection_action(action)` and Chainlit provides `values` via context/inspection. | |
# For TDD, we define how we'll call it or mock the Chainlit environment for calling it. | |
# The @cl.action_callback decorator usually means the function signature is `async def func(action: cl.Action):` | |
# and `values` is accessed via `action.values` or similar, or passed by Chainlit differently. | |
# Let's assume the most common Chainlit pattern: the callback receives `action: cl.Action` and Chainlit somehow makes `values` available, or that the test provides `values` to the function being tested. | |
# Based on Chainlit docs, for actions with inputs (like selects in the same message), the values are passed as a dictionary to the callback. | |
# So the signature should be: async def callback_func(action: cl.Action, values: dict) | |
if not hasattr(app, 'submit_persona_selection_action'): | |
pytest.skip("app.submit_persona_selection_action not yet implemented") | |
# Call the function, passing the values dictionary as the second argument | |
await app.submit_persona_selection_action(action=mock_submit_action, values=submitted_persona_values) | |
# 5. Assertions | |
# Assert that insight_flow_state was updated correctly in the session | |
# Find the call that set 'insight_flow_state' | |
updated_state_call = next((call for call in user_session_set_calls if call[0] == 'insight_flow_state'), None) | |
assert updated_state_call is not None, "insight_flow_state was not set in user_session" | |
updated_state = updated_state_call[1] | |
expected_selected_personas = sorted([pid for pid, selected in submitted_persona_values.items() if selected]) | |
assert sorted(updated_state.get("selected_personas")) == expected_selected_personas, \ | |
f"selected_personas not updated correctly. Expected {expected_selected_personas}, got {updated_state.get('selected_personas')}" | |
# Assert that a confirmation message was sent | |
mock_cl_in_app.Message.assert_any_call(content="Persona selection updated!") | |
# Ensure the send method of the message instance was called | |
# This requires mock_cl_in_app.Message.return_value.send to be an AsyncMock if not already configured by the fixture for all Message instances | |
# The mock_cl fixture does set mock.Message.return_value.send = AsyncMock() | |
assert mock_cl_in_app.Message.return_value.send.called, "Confirmation message was not sent" | |
async def test_on_team_selection_action_updates_state_and_refreshes_ui(mock_cl): | |
"""Test that selecting a team updates state and re-sends the selection UI.""" | |
with patch.object(app, 'cl', new=mock_cl) as mock_cl_in_app: | |
# 1. Setup: Initial state, mock persona factory, selected team | |
initial_selected_personas = ["analytical"] | |
mock_initial_state = {"selected_personas": initial_selected_personas, "query": "test"} | |
# Mock user_session.get and .set | |
# Store the calls to set to verify later | |
user_session_set_calls = [] | |
def mock_user_session_set_side_effect(key, value): | |
user_session_set_calls.append((key, value)) | |
mock_cl_in_app.user_session._actual_session_data[key] = value | |
mock_cl_in_app.user_session._actual_session_data = {"insight_flow_state": mock_initial_state.copy()} | |
# Ensure user_session.set uses our side effect to record calls | |
mock_cl_in_app.user_session.set.side_effect = mock_user_session_set_side_effect | |
# Mock PersonaFactory and its get_available_personas method (needed for UI refresh) | |
available_personas_data = [ | |
{"id": "analytical", "name": "Analytical Abe"}, | |
{"id": "metaphorical", "name": "Metaphorical Max"}, | |
{"id": "futuristic", "name": "Futuristic Fred"}, | |
{"id": "philosophical", "name": "Philosophical Phil"} # Ensure all team members are here | |
] | |
mock_persona_factory_instance = MagicMock() | |
mock_persona_factory_instance.get_available_personas = MagicMock(return_value=available_personas_data) | |
# Ensure user_session.get("persona_factory") returns this mock | |
# and other gets still work from _actual_session_data | |
# original_get = mock_cl_in_app.user_session.get # This was the source of recursion | |
def extended_mock_get(key, default=None): | |
if key == "persona_factory": | |
return mock_persona_factory_instance | |
# Fallback to the _actual_session_data for other keys like "insight_flow_state" | |
return mock_cl_in_app.user_session._actual_session_data.get(key, default) | |
# We need to make sure that the get mock is set up *before* this assignment | |
# The initial mock_cl_in_app.user_session.get was already set up. We are replacing its side_effect. | |
mock_cl_in_app.user_session.get.side_effect = extended_mock_get | |
# Select a team to simulate click (e.g., the first one defined in app.PERSONA_TEAMS) | |
assert len(app.PERSONA_TEAMS) > 0, "PERSONA_TEAMS must be defined in app.py" | |
selected_team_id = list(app.PERSONA_TEAMS.keys())[0] | |
selected_team_info = app.PERSONA_TEAMS[selected_team_id] | |
expected_team_members = sorted(selected_team_info["members"]) | |
mock_team_action = MagicMock(name="handle_team_selection", payload={"team_id": selected_team_id}) | |
# Reset Message mock calls before the action we are testing | |
mock_cl_in_app.Message.reset_mock() | |
mock_cl_in_app.Action.reset_mock() # For actions created during UI refresh | |
mock_cl_in_app.Select.reset_mock() # For selects created during UI refresh | |
# 2. Call the action callback (to be created in app.py) | |
if not hasattr(app, 'handle_team_selection_action'): | |
pytest.skip("app.handle_team_selection_action not yet implemented") | |
await app.handle_team_selection_action(mock_team_action) | |
# 3. Assertions | |
# Assert state update | |
updated_state_call = next((call for call in user_session_set_calls if call[0] == 'insight_flow_state'), None) | |
assert updated_state_call is not None, "insight_flow_state was not set after team selection" | |
updated_state = updated_state_call[1] | |
assert sorted(updated_state.get("selected_personas")) == expected_team_members, \ | |
f"Selected personas in state do not match team members. Expected {expected_team_members}, got {sorted(updated_state.get('selected_personas'))}" | |
# Assert UI refresh: Check that select_personas_action's logic for sending message was re-invoked | |
# This means cl.Message was called again, with cl.Selects and cl.Actions | |
assert mock_cl_in_app.Message.called, "cl.Message was not called to refresh the UI" | |
# Check that cl.Selects were created reflecting the new team | |
assert mock_cl_in_app.Select.call_count == len(available_personas_data) | |
for p_data in available_personas_data: | |
mock_cl_in_app.Select.assert_any_call( | |
id=p_data["id"], | |
label=p_data["name"], | |
initial_value=p_data["id"] in expected_team_members # Key check for refresh | |
) | |
# Check that team actions and update action were part of the refreshed UI | |
num_expected_actions = len(app.PERSONA_TEAMS) + 1 # Teams + Update Individual | |
assert mock_cl_in_app.Action.call_count >= num_expected_actions # Should be at least this many | |
# Verify the 'Update Individual Personas' action was created again | |
mock_cl_in_app.Action.assert_any_call(name="submit_persona_selection", label="Update Individual Personas", payload={}) | |
# Verify team actions were created again | |
for team_id, team_info in app.PERSONA_TEAMS.items(): | |
mock_cl_in_app.Action.assert_any_call(name="handle_team_selection", label=team_info["name"], description=team_info["description"], payload={"team_id": team_id}) | |
# Verify the message structure of the refreshed UI | |
sent_message_args = mock_cl_in_app.Message.call_args | |
assert sent_message_args is not None, "Message to refresh UI was not sent" | |
sent_elements = sent_message_args.kwargs.get("elements", []) | |
sent_actions = sent_message_args.kwargs.get("actions", []) | |
assert len(sent_elements) == len(available_personas_data), "Refreshed UI message elements count mismatch" | |
assert len(sent_actions) == num_expected_actions, "Refreshed UI message actions count mismatch" |