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 @pytest.mark.asyncio 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" @pytest.mark.asyncio 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() @pytest.mark.asyncio 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() @pytest.mark.asyncio 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() @pytest.mark.asyncio 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() @pytest.mark.asyncio 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() @pytest.mark.asyncio 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() @pytest.mark.asyncio 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() @pytest.mark.asyncio 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() @pytest.mark.asyncio 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 @pytest.mark.asyncio 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" @pytest.mark.asyncio 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"