diff --git a/tests/test_team.py b/tests/test_team.py index 06a2e7a3648b..cec090536dc4 100644 --- a/tests/test_team.py +++ b/tests/test_team.py @@ -249,7 +249,7 @@ async def new_team(session, i, user_id=None, member_list=None, model_aliases=Non headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"} data = {"team_alias": "my-new-team"} if user_id is not None: - data["members_with_roles"] = [{"role": "user", "user_id": user_id}] + data["members_with_roles"] = [{"role": "user", "user_id": user_id}] # type: ignore elif member_list is not None: data["members_with_roles"] = member_list @@ -342,7 +342,7 @@ async def test_team_new(): """ user_id = f"{uuid.uuid4()}" async with aiohttp.ClientSession() as session: - new_user(session=session, i=0, user_id=user_id) + await new_user(session=session, i=0, user_id=user_id) tasks = [new_team(session, i, user_id=user_id) for i in range(1, 11)] await asyncio.gather(*tasks) @@ -473,7 +473,7 @@ async def test_team_update_sc_2(): if ( k == "members_with_roles" ): # assert 1 more member (role: "user", user_email: $user_email) - len(new_team_data["data"][k]) == len(team_data[k]) + 1 + assert len(new_team_data["data"][k]) == len(team_data[k]) + 1 elif ( k == "created_at" or k == "updated_at" @@ -608,16 +608,24 @@ async def test_member_delete(dimension): team_data = await new_team(session=session, i=0, member_list=member_list) user_in_team = False - for member in team_data["members_with_roles"]: - if dimension == "user_id" and member["user_id"] == normal_user: - user_in_team = True - elif ( - dimension == "user_email" and member["user_email"] == normal_user_email - ): - user_in_team = True + updated_team_data = None + if dimension == "user_id": + updated_team_data = await delete_member( + session=session, i=0, team_id=team_data["team_id"], user_id=normal_user + ) + elif dimension == "user_email": + updated_team_data = await delete_member( + session=session, + i=0, + team_id=team_data["team_id"], + user_email=normal_user_email, + ) + + if updated_team_data is None: + pytest.fail(f"Failed to delete member for dimension: {dimension}") assert ( - user_in_team is True + user_in_team is False ), "User not in team. Team list={}, User details - id={}, email={}. Dimension={}".format( team_data["members_with_roles"], normal_user, normal_user_email, dimension ) @@ -735,3 +743,178 @@ async def test_users_in_team_budget(): ] == 0.0000001 ) + + +@pytest.mark.asyncio +async def test_team_creation_models_persist(): + """ + Test that models selected during team creation are preserved and not reset. + + This test verifies the fix for the bug where models would get reset + during the team creation process due to useEffect callbacks in the UI. + + Bug: When adding models in the create team modal, the models get reset + Fix: Remove automatic form.setFieldValue("models", []) in useEffect + """ + async with aiohttp.ClientSession() as session: + team_alias = f"test-models-persist-{uuid.uuid4()}" + test_models = ["gpt-3.5-turbo", "gpt-4"] + + # Create team with specific models using the existing new_team helper + # but with explicit models parameter + url = "http://0.0.0.0:4000/team/new" + headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"} + data = { + "team_alias": team_alias, + "models": test_models + } + + async with session.post(url, headers=headers, json=data) as response: + status = response.status + response_text = await response.text() + print(f"Team creation response (Status: {status}): {response_text}") + + if status != 200: + raise Exception(f"Team creation failed with status {status}: {response_text}") + + team_response = await response.json() + + team_id = team_response["team_id"] + + # Verify models were properly set using existing get_team_info helper + team_info = await get_team_info(session, team_id, "sk-1234") + created_models = team_info["team_info"]["models"] + + assert set(created_models) == set(test_models), ( + f"Created team models {created_models} do not match requested models {test_models}" + ) + + print(f"✅ Team created successfully with models: {created_models}") + + +@pytest.mark.asyncio +async def test_team_creation_models_stability_during_updates(): + """ + Test that models remain stable when other team properties are updated. + + This simulates the scenario where models might get reset due to + form state management issues during callbacks. + """ + async with aiohttp.ClientSession() as session: + team_alias = f"test-models-stability-{uuid.uuid4()}" + initial_models = ["gpt-3.5-turbo", "claude-3-sonnet"] + + # Create team with initial models + team_response = await new_team(session, 0) + team_id = team_response["team_id"] + + # Update team with models first + await update_team( + session=session, + i=0, + team_id=team_id, + models=initial_models + ) + + # Update team properties (but not models) + await update_team( + session=session, + i=0, + team_id=team_id, + team_alias=f"updated-{team_alias}", + max_budget=200.0, + tpm_limit=1000 + ) + + # Verify models are still intact after update + team_info = await get_team_info(session, team_id, "sk-1234") + current_models = team_info["team_info"]["models"] + + assert set(current_models) == set(initial_models), ( + f"Models changed after team update. Expected: {initial_models}, Got: {current_models}" + ) + + print(f"✅ Models remained stable during team updates: {current_models}") + + +@pytest.mark.asyncio +async def test_team_creation_with_all_proxy_models(): + """ + Test team creation with "all-proxy-models" option. + + Verifies that the special "all-proxy-models" selection is preserved. + """ + async with aiohttp.ClientSession() as session: + team_alias = f"test-all-models-{uuid.uuid4()}" + + # Create team with all-proxy-models using direct API call + url = "http://0.0.0.0:4000/team/new" + headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"} + data = { + "team_alias": team_alias, + "models": ["all-proxy-models"] + } + + async with session.post(url, headers=headers, json=data) as response: + status = response.status + response_text = await response.text() + + if status != 200: + raise Exception(f"Team creation failed with status {status}: {response_text}") + + team_response = await response.json() + + team_id = team_response["team_id"] + + # Verify all-proxy-models was set + team_info = await get_team_info(session, team_id, "sk-1234") + created_models = team_info["team_info"]["models"] + + # When all-proxy-models is selected, the models list should be empty in the backend + # (empty list means access to all models) + assert created_models == [] or "all-proxy-models" in created_models, ( + f"Team should have all-proxy-models access, got: {created_models}" + ) + + print(f"✅ Team created successfully with all-proxy-models access") + + +@pytest.mark.asyncio +async def test_team_creation_empty_models_list(): + """ + Test team creation with empty models list. + + Verifies that empty models list is handled correctly and defaults to all-proxy-models. + """ + async with aiohttp.ClientSession() as session: + team_alias = f"test-empty-models-{uuid.uuid4()}" + + # Create team with empty models list + url = "http://0.0.0.0:4000/team/new" + headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"} + data = { + "team_alias": team_alias, + "models": [] + } + + async with session.post(url, headers=headers, json=data) as response: + status = response.status + response_text = await response.text() + + if status != 200: + raise Exception(f"Team creation failed with status {status}: {response_text}") + + team_response = await response.json() + + team_id = team_response["team_id"] + + # Verify team was created + team_info = await get_team_info(session, team_id, "sk-1234") + created_models = team_info["team_info"]["models"] + + # Empty models list should default to all-proxy-models access + assert created_models == [], ( + f"Empty models list should result in empty array (all-proxy access), got: {created_models}" + ) + + print(f"✅ Team created with empty models list (all-proxy access): {created_models}") \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/teams.tsx b/ui/litellm-dashboard/src/components/teams.tsx index 87bea9860d55..22693c64dd64 100644 --- a/ui/litellm-dashboard/src/components/teams.tsx +++ b/ui/litellm-dashboard/src/components/teams.tsx @@ -214,7 +214,6 @@ const Teams: React.FC = ({ const models = getOrganizationModels(currentOrgForCreateTeam, userModels); console.log(`models: ${models}`); setModelsToPick(models); - form.setFieldValue("models", []); }, [currentOrgForCreateTeam, userModels]); // Add this useEffect to fetch guardrails