@@ -201,9 +201,15 @@ def _get_models_by_tenant_factory_type(*args, **kwargs):
201201 return []
202202
203203
204+ def _get_models_by_display_name (* args , ** kwargs ):
205+ """Return an empty list for display name lookups in tests."""
206+ return []
207+
208+
204209db_mm_mod .create_model_record = _noop
205210db_mm_mod .delete_model_record = _noop
206211db_mm_mod .get_model_by_display_name = _noop
212+ db_mm_mod .get_models_by_display_name = _get_models_by_display_name
207213db_mm_mod .get_model_records = _get_model_records
208214db_mm_mod .get_models_by_tenant_factory_type = _get_models_by_tenant_factory_type
209215
@@ -590,64 +596,87 @@ async def test_list_provider_models_for_tenant_exception():
590596 assert "Failed to list provider models" in str (exc .value )
591597
592598
593- async def test_update_single_model_for_tenant_success ():
599+ async def test_update_single_model_for_tenant_success_single_model ():
600+ """Update succeeds for a single non-embedding model with no display_name change."""
594601 svc = import_svc ()
595602
596- model = {"model_id" : "1" , "display_name" : "name" }
597- with mock .patch .object (svc , "get_model_by_display_name" , return_value = None ) as mock_get , \
603+ existing_models = [
604+ {"model_id" : 1 , "model_type" : "llm" , "display_name" : "name" },
605+ ]
606+ model_data = {
607+ "model_id" : 1 ,
608+ "display_name" : "name" ,
609+ "description" : "updated" ,
610+ "model_type" : "llm" ,
611+ }
612+
613+ with mock .patch .object (svc , "get_models_by_display_name" , return_value = existing_models ) as mock_get , \
598614 mock .patch .object (svc , "update_model_record" ) as mock_update :
599- await svc .update_single_model_for_tenant ("u1" , "t1" , model )
615+ await svc .update_single_model_for_tenant ("u1" , "t1" , "name" , model_data )
616+
600617 mock_get .assert_called_once_with ("name" , "t1" )
601- mock_update .assert_called_once_with (1 , model , "u1" )
618+ # update_model_record should be called without model_id in the payload
619+ mock_update .assert_called_once_with (
620+ 1 ,
621+ {"display_name" : "name" , "description" : "updated" , "model_type" : "llm" },
622+ "u1" ,
623+ )
602624
603625
604- async def test_update_single_model_for_tenant_conflict ():
626+ async def test_update_single_model_for_tenant_conflict_new_display_name ():
627+ """Updating to a new conflicting display_name raises ValueError."""
605628 svc = import_svc ()
606629
607- model = {"model_id" : "m1" , "display_name" : "name" }
608- with mock .patch .object (svc , "get_model_by_display_name" , return_value = {"model_id" : "other" }):
609- with pytest .raises (Exception ) as exc :
610- await svc .update_single_model_for_tenant ("u1" , "t1" , model )
611- assert "Failed to update model" in str (exc .value )
630+ existing_models = [
631+ {"model_id" : 1 , "model_type" : "llm" , "display_name" : "old_name" },
632+ ]
633+ conflict_models = [
634+ {"model_id" : 2 , "model_type" : "llm" , "display_name" : "new_name" },
635+ ]
636+ model_data = {
637+ "model_id" : 1 ,
638+ "display_name" : "new_name" ,
639+ }
640+
641+ with mock .patch .object (svc , "get_models_by_display_name" , side_effect = [existing_models , conflict_models ]):
642+ with pytest .raises (ValueError ) as exc :
643+ await svc .update_single_model_for_tenant ("u1" , "t1" , "old_name" , model_data )
644+ assert "already in use" in str (exc .value )
612645
613646
614- async def test_update_single_model_for_tenant_same_model_no_conflict ():
615- """Test that updating the same model with same display name doesn't raise conflict ."""
647+ async def test_update_single_model_for_tenant_not_found_raises_lookup_error ():
648+ """If no model is found for current_display_name, raise LookupError ."""
616649 svc = import_svc ()
617650
618- model = {"model_id" : "123" , "display_name" : "existing_name" }
619- # Return the same model_id (as int) to simulate updating the same model
620- with mock .patch .object (svc , "get_model_by_display_name" , return_value = {"model_id" : 123 }) as mock_get , \
621- mock .patch .object (svc , "update_model_record" ) as mock_update :
622- await svc .update_single_model_for_tenant ("u1" , "t1" , model )
623- mock_get .assert_called_once_with ("existing_name" , "t1" )
624- mock_update .assert_called_once_with (123 , model , "u1" )
651+ with mock .patch .object (svc , "get_models_by_display_name" , return_value = []):
652+ with pytest .raises (LookupError ):
653+ await svc .update_single_model_for_tenant ("u1" , "t1" , "missing" , {"display_name" : "x" })
625654
626655
627- async def test_update_single_model_for_tenant_type_conversion ():
628- """Test that string model_id is properly converted to int for comparison ."""
656+ async def test_update_single_model_for_tenant_multi_embedding_updates_both ():
657+ """Updating multi_embedding models updates both embedding and multi_embedding records ."""
629658 svc = import_svc ()
630659
631- model = {"model_id" : "456" , "display_name" : "test_name" }
632- # Return the same model_id as int to test type conversion
633- with mock .patch .object (svc , "get_model_by_display_name" , return_value = {"model_id" : 456 }) as mock_get , \
634- mock .patch .object (svc , "update_model_record" ) as mock_update :
635- await svc .update_single_model_for_tenant ("u1" , "t1" , model )
636- mock_get .assert_called_once_with ("test_name" , "t1" )
637- mock_update .assert_called_once_with (456 , model , "u1" )
638-
660+ existing_models = [
661+ {"model_id" : 10 , "model_type" : "embedding" , "display_name" : "emb_name" },
662+ {"model_id" : 11 , "model_type" : "multi_embedding" , "display_name" : "emb_name" },
663+ ]
664+ model_data = {
665+ "model_id" : 10 ,
666+ "display_name" : "emb_name" ,
667+ "description" : "updated" ,
668+ "model_type" : "multi_embedding" ,
669+ }
639670
640- async def test_update_single_model_for_tenant_different_model_conflict ():
641- """Test that updating with a display name used by a different model raises conflict."""
642- svc = import_svc ( )
671+ with mock . patch . object ( svc , "get_models_by_display_name" , return_value = existing_models ) as mock_get , \
672+ mock . patch . object ( svc , "update_model_record" ) as mock_update :
673+ await svc . update_single_model_for_tenant ( "u1" , "t1" , "emb_name" , model_data )
643674
644- model = {"model_id" : "789" , "display_name" : "conflict_name" }
645- # Return a different model_id to simulate name conflict
646- with mock .patch .object (svc , "get_model_by_display_name" , return_value = {"model_id" : 999 }):
647- with pytest .raises (Exception ) as exc :
648- await svc .update_single_model_for_tenant ("u1" , "t1" , model )
649- assert "Failed to update model" in str (exc .value )
650- assert "Name conflict_name is already in use" in str (exc .value )
675+ mock_get .assert_called_once_with ("emb_name" , "t1" )
676+ # model_type should be stripped from update payload for multi_embedding flow
677+ expected_update = {"display_name" : "emb_name" , "description" : "updated" }
678+ mock_update .assert_any_call (10 , expected_update , "u1" )
679+ mock_update .assert_any_call (11 , expected_update , "u1" )
651680
652681
653682async def test_batch_update_models_for_tenant_success ():
@@ -657,8 +686,8 @@ async def test_batch_update_models_for_tenant_success():
657686 with mock .patch .object (svc , "update_model_record" ) as mock_update :
658687 await svc .batch_update_models_for_tenant ("u1" , "t1" , models )
659688 assert mock_update .call_count == 2
660- mock_update .assert_any_call ("a" , models [0 ], "u1" )
661- mock_update .assert_any_call ("b" , models [1 ], "u1" )
689+ mock_update .assert_any_call ("a" , models [0 ], "u1" , "t1" )
690+ mock_update .assert_any_call ("b" , models [1 ], "u1" , "t1" )
662691
663692
664693async def test_batch_update_models_for_tenant_exception ():
@@ -671,48 +700,62 @@ async def test_batch_update_models_for_tenant_exception():
671700 assert "Failed to batch update models" in str (exc .value )
672701
673702
674- async def test_delete_model_for_tenant_not_found ():
703+ async def test_delete_model_for_tenant_not_found_raises_lookup_error ():
704+ """If no models are found for display_name, raise LookupError."""
675705 svc = import_svc ()
676706
677- with mock .patch .object (svc , "get_model_by_display_name " , return_value = None ):
678- with pytest .raises (Exception ) as exc :
707+ with mock .patch .object (svc , "get_models_by_display_name " , return_value = [] ):
708+ with pytest .raises (LookupError ) :
679709 await svc .delete_model_for_tenant ("u1" , "t1" , "missing" )
680- assert "Failed to delete model" in str (exc .value )
681710
682711
683712async def test_delete_model_for_tenant_embedding_deletes_both ():
713+ """Embedding + multi_embedding models are both deleted and memories cleared."""
684714 svc = import_svc ()
685715
686- # Call sequence: initial -> embedding -> multi_embedding
687- side_effect = [
688- {"model_id" : "id-emb" , "model_type" : "embedding" },
689- {"model_id" : "id-emb" , "model_type" : "embedding" },
690- {"model_id" : "id-multi" , "model_type" : "multi_embedding" },
716+ models = [
717+ {
718+ "model_id" : "id-emb" ,
719+ "model_type" : "embedding" ,
720+ "model_repo" : "openai" ,
721+ "model_name" : "text-embedding-3-small" ,
722+ "max_tokens" : 1536 ,
723+ },
724+ {
725+ "model_id" : "id-multi" ,
726+ "model_type" : "multi_embedding" ,
727+ "model_repo" : "openai" ,
728+ "model_name" : "text-embedding-3-small" ,
729+ "max_tokens" : 1536 ,
730+ },
691731 ]
692- with mock .patch .object (svc , "get_model_by_display_name" , side_effect = side_effect ) as mock_get , \
732+
733+ with mock .patch .object (svc , "get_models_by_display_name" , return_value = models ) as mock_get , \
693734 mock .patch .object (svc , "delete_model_record" ) as mock_delete , \
694735 mock .patch .object (svc , "get_vector_db_core" , return_value = object ()) as mock_get_vdb , \
695736 mock .patch .object (svc , "build_memory_config_for_tenant" , return_value = {}) as mock_build_cfg , \
696737 mock .patch .object (svc , "clear_model_memories" , new = mock .AsyncMock ()) as mock_clear :
697738 await svc .delete_model_for_tenant ("u1" , "t1" , "name" )
739+
740+ mock_get .assert_called_once_with ("name" , "t1" )
698741 assert mock_delete .call_count == 2
699- mock_get .assert_called ()
700742 mock_get_vdb .assert_called_once ()
701743 mock_build_cfg .assert_called_once_with ("t1" )
702- # Best-effort cleanup may call once or twice depending on state
703- assert mock_clear .await_count >= 1
744+ # Best-effort cleanup should be attempted for both records
745+ assert mock_clear .await_count == 2
704746
705747
706748@pytest .mark .asyncio
707749async def test_delete_model_for_tenant_cleanup_inner_exception (caplog ):
708750 svc = import_svc ()
709751
710- side_effect = [
711- {"model_id" : "id-emb" , "model_type" : "embedding" },
712- {"model_id" : "id-emb" , "model_type" : "embedding" },
713- {"model_id" : "id-multi" , "model_type" : "multi_embedding" },
752+ models = [
753+ {"model_id" : "id-emb" , "model_type" : "embedding" ,
754+ "model_repo" : "r" , "model_name" : "n" , "max_tokens" : 1 },
755+ {"model_id" : "id-multi" , "model_type" : "multi_embedding" ,
756+ "model_repo" : "r" , "model_name" : "n" , "max_tokens" : 1 },
714757 ]
715- with mock .patch .object (svc , "get_model_by_display_name " , side_effect = side_effect ), \
758+ with mock .patch .object (svc , "get_models_by_display_name " , return_value = models ), \
716759 mock .patch .object (svc , "delete_model_record" ) as mock_delete , \
717760 mock .patch .object (svc , "get_vector_db_core" , return_value = object ()), \
718761 mock .patch .object (svc , "build_memory_config_for_tenant" , return_value = {}), \
@@ -730,12 +773,11 @@ async def test_delete_model_for_tenant_cleanup_inner_exception(caplog):
730773async def test_delete_model_for_tenant_cleanup_outer_exception (caplog ):
731774 svc = import_svc ()
732775
733- side_effect = [
734- {"model_id" : "id-emb" , "model_type" : "embedding" },
776+ models = [
735777 {"model_id" : "id-emb" , "model_type" : "embedding" },
736778 {"model_id" : "id-multi" , "model_type" : "multi_embedding" },
737779 ]
738- with mock .patch .object (svc , "get_model_by_display_name " , side_effect = side_effect ), \
780+ with mock .patch .object (svc , "get_models_by_display_name " , return_value = models ), \
739781 mock .patch .object (svc , "delete_model_record" ) as mock_delete , \
740782 mock .patch .object (svc , "get_vector_db_core" , side_effect = Exception ("vdb_down" )), \
741783 mock .patch .object (svc , "build_memory_config_for_tenant" , return_value = {}):
@@ -749,12 +791,19 @@ async def test_delete_model_for_tenant_cleanup_outer_exception(caplog):
749791
750792
751793async def test_delete_model_for_tenant_non_embedding ():
794+ """Non-embedding model deletes a single record without memory cleanup."""
752795 svc = import_svc ()
753796
754- with mock .patch .object (svc , "get_model_by_display_name" , return_value = {"model_id" : "id" , "model_type" : "llm" }), \
755- mock .patch .object (svc , "delete_model_record" ) as mock_delete :
797+ models = [
798+ {"model_id" : "id" , "model_type" : "llm" },
799+ ]
800+ with mock .patch .object (svc , "get_models_by_display_name" , return_value = models ), \
801+ mock .patch .object (svc , "delete_model_record" ) as mock_delete , \
802+ mock .patch .object (svc , "get_vector_db_core" ) as mock_get_vdb :
756803 await svc .delete_model_for_tenant ("u1" , "t1" , "name" )
757804 mock_delete .assert_called_once_with ("id" , "u1" , "t1" )
805+ # For non-embedding models we should not prepare vector DB cleanup
806+ mock_get_vdb .assert_not_called ()
758807
759808
760809async def test_list_models_for_tenant_success ():
0 commit comments