Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 3 additions & 17 deletions bt_servant_engine/apps/api/routes/admin_datastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
MergeRequest,
)

router = APIRouter()
router = APIRouter(prefix="/admin")

MAX_CACHE_SAMPLE_LIMIT = 100

Expand All @@ -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,
Expand Down Expand Up @@ -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")


Expand All @@ -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")


Expand Down
31 changes: 20 additions & 11 deletions tests/apps/api/routes/test_admin_cache_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -106,27 +107,27 @@ 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


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"
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -167,21 +172,25 @@ 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"
assert data["sample_limit"] == sample_limit
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"
2 changes: 1 addition & 1 deletion tests/test_api_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 21 additions & 19 deletions tests/test_chroma_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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"]

Expand All @@ -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"

Expand All @@ -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"
Expand All @@ -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()
Expand All @@ -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"

Expand All @@ -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"
Expand All @@ -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"

Expand All @@ -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"
Expand Down
14 changes: 8 additions & 6 deletions tests/test_chroma_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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"):
Expand Down Expand Up @@ -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",
Expand All @@ -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"):
Expand Down