Livrare LOT 1 - Didi
This commit is contained in:
commit
5380c3fc63
990 changed files with 133308 additions and 0 deletions
0
ai_platform/modules/dashboard/tests/__init__.py
Normal file
0
ai_platform/modules/dashboard/tests/__init__.py
Normal file
96
ai_platform/modules/dashboard/tests/test_catalog.py
Normal file
96
ai_platform/modules/dashboard/tests/test_catalog.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""CRUD tests for the DB-backed model/extractor catalog (Val 2).
|
||||
|
||||
Uses an in-memory SQLite async DB — no Postgres needed. Route handlers are
|
||||
called directly with an injected session + principal.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
||||
from dashboard.api.routes import catalog
|
||||
from dashboard.db.models import Base
|
||||
|
||||
# Prefer a real Postgres (matches production) when TEST_DATABASE_URL is set;
|
||||
# fall back to in-memory SQLite for quick local runs.
|
||||
_DB_URL = os.environ.get("TEST_DATABASE_URL", "sqlite+aiosqlite:///:memory:")
|
||||
|
||||
# HTTPException is imported lazily so the module imports even without fastapi
|
||||
from fastapi import HTTPException # noqa: E402
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def session():
|
||||
engine = create_async_engine(_DB_URL)
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
maker = async_sessionmaker(engine, expire_on_commit=False)
|
||||
async with maker() as s:
|
||||
yield s
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def test_crud_lifecycle(session):
|
||||
created = await catalog.create_catalog(
|
||||
catalog.CatalogIn(
|
||||
name="qwen3.5", service="llm", supports_gpu=True, context_length=32768,
|
||||
quantization="awq",
|
||||
),
|
||||
principal="tester",
|
||||
session=session,
|
||||
)
|
||||
assert created["name"] == "qwen3.5"
|
||||
assert created["supports_gpu"] is True
|
||||
eid = created["id"]
|
||||
|
||||
listed = await catalog.list_catalog(session=session)
|
||||
assert listed["total"] == 1
|
||||
|
||||
filtered = await catalog.list_catalog(service="embeddings", session=session)
|
||||
assert filtered["total"] == 0
|
||||
|
||||
updated = await catalog.update_catalog(
|
||||
eid, catalog.CatalogPatch(enabled=False, notes="retired"),
|
||||
principal="tester", session=session,
|
||||
)
|
||||
assert updated["enabled"] is False
|
||||
assert updated["notes"] == "retired"
|
||||
|
||||
deleted = await catalog.delete_catalog(eid, principal="tester", session=session)
|
||||
assert deleted["deleted"] == eid
|
||||
assert (await catalog.list_catalog(session=session))["total"] == 0
|
||||
|
||||
|
||||
async def test_duplicate_rejected(session):
|
||||
await catalog.create_catalog(
|
||||
catalog.CatalogIn(name="bge-m3", service="embeddings"),
|
||||
principal="t", session=session,
|
||||
)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await catalog.create_catalog(
|
||||
catalog.CatalogIn(name="bge-m3", service="embeddings"),
|
||||
principal="t", session=session,
|
||||
)
|
||||
assert exc.value.status_code == 409
|
||||
|
||||
|
||||
async def test_invalid_kind_rejected(session):
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await catalog.create_catalog(
|
||||
catalog.CatalogIn(name="x", service="llm", kind="bogus"),
|
||||
principal="t", session=session,
|
||||
)
|
||||
assert exc.value.status_code == 422
|
||||
|
||||
|
||||
async def test_update_missing_404(session):
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await catalog.update_catalog(
|
||||
999, catalog.CatalogPatch(enabled=True), principal="t", session=session
|
||||
)
|
||||
assert exc.value.status_code == 404
|
||||
118
ai_platform/modules/dashboard/tests/test_monitoring.py
Normal file
118
ai_platform/modules/dashboard/tests/test_monitoring.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"""Tests for the central AI monitoring routes (Val 1).
|
||||
|
||||
External sources (service health, RabbitMQ, Prometheus) are mocked with respx /
|
||||
monkeypatch — no real infra needed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
from dashboard.api.routes import monitoring
|
||||
from dashboard.config import DashboardSettings, SettingsCache
|
||||
|
||||
|
||||
def _settings(**overrides) -> DashboardSettings:
|
||||
base = {
|
||||
"database_url": "postgresql+asyncpg://u:p@localhost/db",
|
||||
"external_url": "http://localhost:51300",
|
||||
}
|
||||
base.update(overrides)
|
||||
s = DashboardSettings(**base)
|
||||
SettingsCache.set(s)
|
||||
return s
|
||||
|
||||
|
||||
# --- services aggregation -------------------------------------------------
|
||||
|
||||
|
||||
async def test_services_summary(monkeypatch):
|
||||
_settings()
|
||||
|
||||
async def fake_probe(client, mid, url):
|
||||
status = "down" if mid == "video" else "healthy"
|
||||
return {"module": mid, "url": url, "status": status, "latency_ms": 1.0}
|
||||
|
||||
monkeypatch.setattr(monitoring, "_probe", fake_probe)
|
||||
out = await monitoring.monitoring_services()
|
||||
|
||||
assert out["summary"]["total"] == len(monitoring._service_endpoints())
|
||||
assert out["summary"]["down"] == 1
|
||||
assert out["summary"]["healthy"] == out["summary"]["total"] - 1
|
||||
assert any(s["module"] == "video" and s["status"] == "down" for s in out["services"])
|
||||
|
||||
|
||||
# --- RabbitMQ queues ------------------------------------------------------
|
||||
|
||||
|
||||
async def test_queues_disabled_when_unset():
|
||||
_settings(rabbitmq_mgmt_url=None)
|
||||
out = await monitoring.monitoring_queues()
|
||||
assert out["enabled"] is False
|
||||
assert out["queues"] == []
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_queues_parsed_and_sorted():
|
||||
_settings(rabbitmq_mgmt_url="http://rmq:15672")
|
||||
respx.get("http://rmq:15672/api/queues").mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json=[
|
||||
{"name": "low", "vhost": "/", "messages": 2, "messages_ready": 2,
|
||||
"messages_unacknowledged": 0, "consumers": 1, "state": "running"},
|
||||
{"name": "busy", "vhost": "/", "messages": 50, "messages_ready": 48,
|
||||
"messages_unacknowledged": 2, "consumers": 3, "state": "running"},
|
||||
],
|
||||
)
|
||||
)
|
||||
out = await monitoring.monitoring_queues()
|
||||
assert out["enabled"] is True
|
||||
# sorted by messages desc
|
||||
assert [q["name"] for q in out["queues"]] == ["busy", "low"]
|
||||
assert out["queues"][0]["unacked"] == 2
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_queues_failopen_on_error():
|
||||
_settings(rabbitmq_mgmt_url="http://rmq:15672")
|
||||
respx.get("http://rmq:15672/api/queues").mock(side_effect=httpx.ConnectError("x"))
|
||||
out = await monitoring.monitoring_queues()
|
||||
assert out["enabled"] is True
|
||||
assert out["queues"] == []
|
||||
assert "error" in out
|
||||
|
||||
|
||||
# --- Prometheus latency ---------------------------------------------------
|
||||
|
||||
|
||||
async def test_latency_disabled_when_unset():
|
||||
_settings(prometheus_url=None)
|
||||
out = await monitoring.monitoring_latency()
|
||||
assert out["enabled"] is False
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_latency_parsed_per_job():
|
||||
_settings(prometheus_url="http://prom:9090")
|
||||
# Same response for all three quantile queries → equal p50/p90/p99.
|
||||
respx.get("http://prom:9090/api/v1/query").mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"data": {
|
||||
"result": [
|
||||
{"metric": {"job": "llm"}, "value": [0, "0.250"]},
|
||||
{"metric": {"job": "web"}, "value": [0, "1.500"]},
|
||||
]
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
out = await monitoring.monitoring_latency()
|
||||
assert out["enabled"] is True
|
||||
by_job = {s["job"]: s for s in out["services"]}
|
||||
assert by_job["llm"]["p50_ms"] == 250.0 # 0.250s → 250ms
|
||||
assert by_job["web"]["p99_ms"] == 1500.0
|
||||
38
ai_platform/modules/dashboard/tests/test_rbac.py
Normal file
38
ai_platform/modules/dashboard/tests/test_rbac.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
"""RBAC wiring invariants (Val 2).
|
||||
|
||||
Asserts that human-facing read routers carry the auth dependency, while the
|
||||
service-to-service routers (config polling, ingest) stay open — breaking the
|
||||
latter would cut config propagation / event ingestion to the AI services.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dashboard.api.routes import (
|
||||
audit,
|
||||
catalog,
|
||||
config,
|
||||
history,
|
||||
ingest,
|
||||
monitoring,
|
||||
proxy,
|
||||
stats,
|
||||
)
|
||||
|
||||
|
||||
def test_human_read_routers_are_protected():
|
||||
for r in (
|
||||
monitoring.router,
|
||||
stats.router,
|
||||
history.router,
|
||||
audit.router,
|
||||
proxy.router,
|
||||
catalog.router,
|
||||
):
|
||||
assert len(r.dependencies) >= 1, "expected router-level auth dependency"
|
||||
|
||||
|
||||
def test_service_routers_stay_open():
|
||||
# config (polled by AI services) and ingest (service-to-service) must NOT
|
||||
# gain router-level auth — that would break the platform.
|
||||
assert len(config.router.dependencies) == 0
|
||||
assert len(ingest.router.dependencies) == 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue