Skip to content

Commit 62c3566

Browse files
committed
🐛 Add unitest
1 parent 691ed3c commit 62c3566

File tree

2 files changed

+147
-73
lines changed

2 files changed

+147
-73
lines changed

test/backend/app/test_model_managment_app.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@
66
from http import HTTPStatus
77
from unittest.mock import patch, MagicMock
88

9-
# Add path for correct imports
10-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../../backend"))
9+
# Add project root to sys.path so that the top-level `backend` package is importable
10+
PROJECT_ROOT = os.path.join(os.path.dirname(__file__), "../../..")
11+
if PROJECT_ROOT not in sys.path:
12+
sys.path.insert(0, PROJECT_ROOT)
13+
14+
# Also add the backend source directory so that subpackages like `consts` can be imported directly
15+
BACKEND_ROOT = os.path.join(PROJECT_ROOT, "backend")
16+
if BACKEND_ROOT not in sys.path:
17+
sys.path.insert(0, BACKEND_ROOT)
1118

1219
# Patch environment variables before any imports that might use them
1320
os.environ.setdefault('MINIO_ENDPOINT', 'http://localhost:9000')
@@ -513,12 +520,21 @@ async def mock_update_single(*args, **kwargs):
513520
"provider": "huggingface"
514521
}
515522
response = client.post(
516-
"/model/update", json=update_data, headers=auth_header)
523+
"/model/update",
524+
params={"display_name": "Updated Test Model"},
525+
json=update_data,
526+
headers=auth_header,
527+
)
517528

518529
assert response.status_code == HTTPStatus.OK
519530
data = response.json()
520531
assert "Model updated successfully" in data["message"]
521-
mock_update.assert_called_once_with(user_credentials[0], user_credentials[1], update_data)
532+
mock_update.assert_called_once_with(
533+
user_credentials[0],
534+
user_credentials[1],
535+
"Updated Test Model",
536+
update_data,
537+
)
522538

523539

524540
@pytest.mark.asyncio
@@ -527,8 +543,8 @@ async def test_update_single_model_conflict(client, auth_header, user_credential
527543
mocker.patch('apps.model_managment_app.get_current_user_id', return_value=user_credentials)
528544

529545
mock_update = mocker.patch(
530-
'apps.model_managment_app.update_single_model_for_tenant',
531-
side_effect=ValueError("Name 'Conflicting Name' is already in use, please choose another display name")
546+
'apps.model_managment_app.update_single_model_for_tenant',
547+
side_effect=ValueError("Name 'Conflicting Name' is already in use, please choose another display name"),
532548
)
533549

534550
update_data = {
@@ -541,13 +557,22 @@ async def test_update_single_model_conflict(client, auth_header, user_credential
541557
"provider": "huggingface"
542558
}
543559
response = client.post(
544-
"/model/update", json=update_data, headers=auth_header)
560+
"/model/update",
561+
params={"display_name": "Conflicting Name"},
562+
json=update_data,
563+
headers=auth_header,
564+
)
545565

546566
assert response.status_code == HTTPStatus.CONFLICT
547567
data = response.json()
548568
# Now we return the actual error message
549569
assert "Name 'Conflicting Name' is already in use" in data.get("detail", "")
550-
mock_update.assert_called_once_with(user_credentials[0], user_credentials[1], update_data)
570+
mock_update.assert_called_once_with(
571+
user_credentials[0],
572+
user_credentials[1],
573+
"Conflicting Name",
574+
update_data,
575+
)
551576

552577

553578
# Tests for /model/batch_update endpoint

test/backend/services/test_model_management_service.py

Lines changed: 114 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
204209
db_mm_mod.create_model_record = _noop
205210
db_mm_mod.delete_model_record = _noop
206211
db_mm_mod.get_model_by_display_name = _noop
212+
db_mm_mod.get_models_by_display_name = _get_models_by_display_name
207213
db_mm_mod.get_model_records = _get_model_records
208214
db_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

653682
async 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

664693
async 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

683712
async 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
707749
async 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):
730773
async 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

751793
async 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

760809
async def test_list_models_for_tenant_success():

0 commit comments

Comments
 (0)