diff --git a/bt_servant_engine/apps/api/routes/admin_datastore.py b/bt_servant_engine/apps/api/routes/admin_datastore.py index 7a81330..6a64cd2 100644 --- a/bt_servant_engine/apps/api/routes/admin_datastore.py +++ b/bt_servant_engine/apps/api/routes/admin_datastore.py @@ -23,7 +23,7 @@ MergeRequest, ) -router = APIRouter() +router = APIRouter(prefix="/admin") MAX_CACHE_SAMPLE_LIMIT = 100 @@ -50,20 +50,6 @@ async def add_document( return JSONResponse(status_code=status.HTTP_200_OK, content=dict(payload)) -@router.post("/add-document") -async def add_document_alias( - document: Document, - _: AuthDependency, - service: AdminServiceDependency, -) -> JSONResponse: - """Backwards-compatible alias for `/chroma/add-document`.""" - try: - payload = service.add_document(document) - except CollectionNotFoundError as exc: - return _http_error(status.HTTP_404_NOT_FOUND, str(exc)) - return JSONResponse(status_code=status.HTTP_200_OK, content=dict(payload)) - - @router.post("/chroma/collections") async def create_collection_endpoint( payload: CollectionCreate, @@ -259,7 +245,7 @@ async def get_document_text_endpoint( @router.api_route("/chroma", methods=["GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD"]) async def chroma_root(_: AuthDependency) -> None: - """Catch-all /chroma root to enforce admin auth and 404 unknown paths.""" + """Catch-all /admin/chroma root to enforce admin auth and 404 unknown paths.""" raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found") @@ -268,7 +254,7 @@ async def chroma_root(_: AuthDependency) -> None: methods=["GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD"], ) async def chroma_catch_all(_path: str, _: AuthDependency) -> None: - """Catch-all for any /chroma subpath to enforce admin auth and 404.""" + """Catch-all for any /admin/chroma subpath to enforce admin auth and 404.""" raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found") diff --git a/tests/apps/api/routes/test_admin_cache_endpoints.py b/tests/apps/api/routes/test_admin_cache_endpoints.py index 2a68958..e16c60d 100644 --- a/tests/apps/api/routes/test_admin_cache_endpoints.py +++ b/tests/apps/api/routes/test_admin_cache_endpoints.py @@ -15,6 +15,7 @@ from bt_servant_engine.services.admin import datastore as admin_datastore_service MAX_CACHE_SAMPLE_LIMIT = admin_datastore_router.MAX_CACHE_SAMPLE_LIMIT +ADMIN_PREFIX = "/admin" class _StubCache: @@ -106,7 +107,7 @@ def _make_client(monkeypatch) -> tuple[TestClient, _StubCacheManager]: def test_clear_all_caches_endpoint(monkeypatch): client, stub = _make_client(monkeypatch) - resp = client.post("/cache/clear") + resp = client.post(f"{ADMIN_PREFIX}/cache/clear") assert resp.status_code == HTTPStatus.OK assert resp.json()["status"] == "cleared" assert stub.clear_all_called @@ -114,19 +115,19 @@ def test_clear_all_caches_endpoint(monkeypatch): def test_clear_named_cache_endpoint(monkeypatch): client, stub = _make_client(monkeypatch) - resp = client.post("/cache/selection/clear") + resp = client.post(f"{ADMIN_PREFIX}/cache/selection/clear") assert resp.status_code == HTTPStatus.OK assert resp.json()["cache"] == "selection" assert stub.clear_called == ["selection"] - resp = client.post("/cache/unknown/clear") + resp = client.post(f"{ADMIN_PREFIX}/cache/unknown/clear") assert resp.status_code == HTTPStatus.NOT_FOUND assert resp.json()["detail"]["error"] == "Cache 'unknown' not found" def test_prune_all_caches_endpoint(monkeypatch): client, stub = _make_client(monkeypatch) - resp = client.post("/cache/clear", params={"older_than_days": 1}) + resp = client.post(f"{ADMIN_PREFIX}/cache/clear", params={"older_than_days": 1}) assert resp.status_code == HTTPStatus.OK payload = resp.json() assert payload["status"] == "pruned" @@ -138,7 +139,9 @@ def test_prune_all_caches_endpoint(monkeypatch): def test_prune_named_cache_endpoint(monkeypatch): client, stub = _make_client(monkeypatch) days = 2 - resp = client.post("/cache/selection/clear", params={"older_than_days": days}) + resp = client.post( + f"{ADMIN_PREFIX}/cache/selection/clear", params={"older_than_days": days} + ) assert resp.status_code == HTTPStatus.OK payload = resp.json() assert payload["status"] == "pruned" @@ -149,15 +152,17 @@ def test_prune_named_cache_endpoint(monkeypatch): def test_prune_invalid_params(monkeypatch): client, _ = _make_client(monkeypatch) - resp = client.post("/cache/clear", params={"older_than_days": -1}) + resp = client.post(f"{ADMIN_PREFIX}/cache/clear", params={"older_than_days": -1}) assert resp.status_code == HTTPStatus.BAD_REQUEST - resp = client.post("/cache/selection/clear", params={"older_than_days": 0}) + resp = client.post( + f"{ADMIN_PREFIX}/cache/selection/clear", params={"older_than_days": 0} + ) assert resp.status_code == HTTPStatus.BAD_REQUEST def test_get_cache_stats_endpoint(monkeypatch): client, _ = _make_client(monkeypatch) - resp = client.get("/cache/stats") + resp = client.get(f"{ADMIN_PREFIX}/cache/stats") assert resp.status_code == HTTPStatus.OK data = resp.json() assert data["enabled"] is True @@ -167,7 +172,9 @@ def test_get_cache_stats_endpoint(monkeypatch): def test_inspect_cache_endpoint(monkeypatch): client, stub = _make_client(monkeypatch) sample_limit = 5 - resp = client.get("/cache/selection", params={"sample_limit": sample_limit}) + resp = client.get( + f"{ADMIN_PREFIX}/cache/selection", params={"sample_limit": sample_limit} + ) assert resp.status_code == HTTPStatus.OK data = resp.json() assert data["name"] == "selection" @@ -175,13 +182,15 @@ def test_inspect_cache_endpoint(monkeypatch): assert stub.cache_obj.sample_limit == sample_limit assert data["samples"] - bad_resp = client.get("/cache/selection", params={"sample_limit": 0}) + bad_resp = client.get( + f"{ADMIN_PREFIX}/cache/selection", params={"sample_limit": 0} + ) assert bad_resp.status_code == HTTPStatus.BAD_REQUEST assert ( bad_resp.json()["detail"]["error"] == f"sample_limit must be between 1 and {MAX_CACHE_SAMPLE_LIMIT}" ) - missing = client.get("/cache/missing") + missing = client.get(f"{ADMIN_PREFIX}/cache/missing") assert missing.status_code == HTTPStatus.NOT_FOUND assert missing.json()["detail"]["error"] == "Cache 'missing' not found" diff --git a/tests/test_api_app.py b/tests/test_api_app.py index 89cb4fc..2907611 100644 --- a/tests/test_api_app.py +++ b/tests/test_api_app.py @@ -18,7 +18,7 @@ def test_create_app_has_routes(): if (path := getattr(route, "path", getattr(route, "path_format", ""))) } assert "/alive" in paths - assert any(path.startswith("/chroma") for path in paths) + assert any(path.startswith("/admin/chroma") for path in paths) assert hasattr(app.state, "services") assert app.state.services.intent_router is not None assert app.state.services.chroma is not None diff --git a/tests/test_chroma_endpoints.py b/tests/test_chroma_endpoints.py index 2d98bec..6aab4f3 100644 --- a/tests/test_chroma_endpoints.py +++ b/tests/test_chroma_endpoints.py @@ -12,6 +12,8 @@ from bt_servant_engine.core.config import config as app_config EXPECTED_COLLECTION_DOCUMENT_COUNT = 3 +ADMIN_PREFIX = "/admin" +ADMIN_CHROMA_PREFIX = f"{ADMIN_PREFIX}/chroma" class DummyEmbeddingFunction: @@ -51,26 +53,26 @@ def test_create_and_delete_collection(monkeypatch, tmp_path): client = TestClient(create_app(build_default_service_container())) # Create a collection - resp = client.post("/chroma/collections", json={"name": "testcol"}) + resp = client.post(f"{ADMIN_CHROMA_PREFIX}/collections", json={"name": "testcol"}) assert resp.status_code == HTTPStatus.CREATED, resp.text assert resp.json()["status"] == "created" # Creating the same collection again should yield 409 - resp = client.post("/chroma/collections", json={"name": "testcol"}) + resp = client.post(f"{ADMIN_CHROMA_PREFIX}/collections", json={"name": "testcol"}) assert resp.status_code == HTTPStatus.CONFLICT assert resp.json()["error"] == "Collection already exists" # Delete the collection - resp = client.delete("/chroma/collections/testcol") + resp = client.delete(f"{ADMIN_CHROMA_PREFIX}/collections/testcol") assert resp.status_code == HTTPStatus.NO_CONTENT # Deleting again should yield 404 - resp = client.delete("/chroma/collections/testcol") + resp = client.delete(f"{ADMIN_CHROMA_PREFIX}/collections/testcol") assert resp.status_code == HTTPStatus.NOT_FOUND assert resp.json()["error"] == "Collection not found" # Invalid name cases - resp = client.post("/chroma/collections", json={"name": " "}) + resp = client.post(f"{ADMIN_CHROMA_PREFIX}/collections", json={"name": " "}) assert resp.status_code == HTTPStatus.BAD_REQUEST err = resp.json()["error"] assert ("Invalid" in err) or ("must be non-empty" in err) @@ -88,16 +90,16 @@ def test_list_collections_and_delete_document(monkeypatch, tmp_path): client = TestClient(create_app(build_default_service_container())) # Initially, list is empty - resp = client.get("/chroma/collections") + resp = client.get(f"{ADMIN_CHROMA_PREFIX}/collections") assert resp.status_code == HTTPStatus.OK assert resp.json()["collections"] == [] # Create a collection - resp = client.post("/chroma/collections", json={"name": "col1"}) + resp = client.post(f"{ADMIN_CHROMA_PREFIX}/collections", json={"name": "col1"}) assert resp.status_code == HTTPStatus.CREATED # List should include col1 - resp = client.get("/chroma/collections") + resp = client.get(f"{ADMIN_CHROMA_PREFIX}/collections") assert resp.status_code == HTTPStatus.OK assert "col1" in resp.json()["collections"] @@ -109,15 +111,15 @@ def test_list_collections_and_delete_document(monkeypatch, tmp_path): "text": "Hello world", "metadata": {"k": "v"}, } - resp = client.post("/add-document", json=doc_payload) + resp = client.post(f"{ADMIN_CHROMA_PREFIX}/add-document", json=doc_payload) assert resp.status_code == HTTPStatus.OK # Delete the document - resp = client.delete("/chroma/collections/col1/documents/42") + resp = client.delete(f"{ADMIN_CHROMA_PREFIX}/collections/col1/documents/42") assert resp.status_code == HTTPStatus.NO_CONTENT # Deleting again should yield 404 - resp = client.delete("/chroma/collections/col1/documents/42") + resp = client.delete(f"{ADMIN_CHROMA_PREFIX}/collections/col1/documents/42") assert resp.status_code == HTTPStatus.NOT_FOUND assert resp.json()["error"] == "Document not found" @@ -132,7 +134,7 @@ def test_admin_auth_401_json_body(monkeypatch): client = TestClient(create_app(build_default_service_container())) # No Authorization headers provided - resp = client.get("/chroma/collections") + resp = client.get(f"{ADMIN_CHROMA_PREFIX}/collections") assert resp.status_code == HTTPStatus.UNAUTHORIZED # JSON body present with detail assert resp.headers.get("WWW-Authenticate") == "Bearer" @@ -145,15 +147,15 @@ def test_admin_auth_401_json_body(monkeypatch): def test_chroma_root_unauthorized_returns_401(monkeypatch): - """Unauthorized request to /chroma returns 401 and WWW-Authenticate header.""" - # Unauthorized access to /chroma should yield 401 (not 404) + """Unauthorized request to /admin/chroma returns 401 and WWW-Authenticate header.""" + # Unauthorized access to /admin/chroma should yield 401 (not 404) # api imported at module scope monkeypatch.setattr(app_config, "ENABLE_ADMIN_AUTH", True) monkeypatch.setattr(app_config, "ADMIN_API_TOKEN", "secret") client = TestClient(create_app(build_default_service_container())) - resp = client.get("/chroma") + resp = client.get(ADMIN_CHROMA_PREFIX) assert resp.status_code == HTTPStatus.UNAUTHORIZED assert resp.headers.get("WWW-Authenticate") == "Bearer" data = resp.json() @@ -176,7 +178,7 @@ def test_count_documents_in_collection(monkeypatch, tmp_path): client = TestClient(create_app(build_default_service_container())) # Missing collection -> 404 - resp = client.get("/chroma/collections/missing/count") + resp = client.get(f"{ADMIN_CHROMA_PREFIX}/collections/missing/count") assert resp.status_code == HTTPStatus.NOT_FOUND assert resp.json()["error"] == "Collection not found" @@ -190,7 +192,7 @@ def test_count_documents_in_collection(monkeypatch, tmp_path): ) # Count should be 3 - resp = client.get("/chroma/collections/countcol/count") + resp = client.get(f"{ADMIN_CHROMA_PREFIX}/collections/countcol/count") assert resp.status_code == HTTPStatus.OK body = resp.json() assert body["collection"] == "countcol" @@ -209,7 +211,7 @@ def test_list_document_ids_endpoint(monkeypatch, tmp_path): client = TestClient(create_app(build_default_service_container())) # Missing collection -> 404 - resp = client.get("/chroma/collection/missing/ids") + resp = client.get(f"{ADMIN_CHROMA_PREFIX}/collection/missing/ids") assert resp.status_code == HTTPStatus.NOT_FOUND assert resp.json()["error"] == "Collection not found" @@ -223,7 +225,7 @@ def test_list_document_ids_endpoint(monkeypatch, tmp_path): ) # Fetch ids via endpoint - resp = client.get("/chroma/collection/idscol/ids") + resp = client.get(f"{ADMIN_CHROMA_PREFIX}/collection/idscol/ids") assert resp.status_code == HTTPStatus.OK body = resp.json() assert body["collection"] == "idscol" diff --git a/tests/test_chroma_merge.py b/tests/test_chroma_merge.py index dce1ae6..594df83 100644 --- a/tests/test_chroma_merge.py +++ b/tests/test_chroma_merge.py @@ -27,6 +27,8 @@ MERGE_POLL_INTERVAL_SECONDS = 0.05 CANCEL_BATCH_SIZE = 5 CANCEL_START_DELAY_SECONDS = 0.1 +ADMIN_PREFIX = "/admin" +ADMIN_CHROMA_PREFIX = f"{ADMIN_PREFIX}/chroma" class FakeCollection: @@ -127,7 +129,7 @@ def test_dry_run_duplicates_preview_limit(fake_chroma): client = TestClient(create_app(build_default_service_container())) resp = client.post( - "/chroma/collections/dst/merge", + f"{ADMIN_CHROMA_PREFIX}/collections/dst/merge", json= { "source": "src", @@ -160,7 +162,7 @@ def test_merge_create_new_id_with_tags_and_copy(fake_chroma): client = TestClient(create_app(build_default_service_container())) # Start merge resp = client.post( - "/chroma/collections/dst/merge", + f"{ADMIN_CHROMA_PREFIX}/collections/dst/merge", json= { "source": "src", @@ -179,7 +181,7 @@ def test_merge_create_new_id_with_tags_and_copy(fake_chroma): # Poll for completion with generous timeout for _ in range(MERGE_POLL_ATTEMPTS): # 50s total timeout for CI environments - st = client.get(f"/chroma/merge-tasks/{task_id}") + st = client.get(f"{ADMIN_CHROMA_PREFIX}/merge-tasks/{task_id}") assert st.status_code == HTTPStatus.OK data = st.json() if data["status"] in ("completed", "failed"): @@ -215,7 +217,7 @@ def test_cancel_merge(fake_chroma): client = TestClient(create_app(build_default_service_container())) resp = client.post( - "/chroma/collections/dst/merge", + f"{ADMIN_CHROMA_PREFIX}/collections/dst/merge", json= { "source": "src", @@ -232,13 +234,13 @@ def test_cancel_merge(fake_chroma): time.sleep(CANCEL_START_DELAY_SECONDS) # Request cancel - cancel = client.delete(f"/chroma/merge-tasks/{task_id}") + cancel = client.delete(f"{ADMIN_CHROMA_PREFIX}/merge-tasks/{task_id}") # If the task already completed, cancellation can return 409 assert cancel.status_code in (HTTPStatus.ACCEPTED, HTTPStatus.CONFLICT) # Wait for cancel to be acknowledged with generous timeout for _ in range(MERGE_POLL_ATTEMPTS): # 50s timeout for CI environments - st = client.get(f"/chroma/merge-tasks/{task_id}") + st = client.get(f"{ADMIN_CHROMA_PREFIX}/merge-tasks/{task_id}") assert st.status_code == HTTPStatus.OK data = st.json() if data["status"] in ("cancelled", "completed", "failed"):