153 lines
5.6 KiB
Python
153 lines
5.6 KiB
Python
from pathlib import Path
|
|
|
|
from geniehive_control.main import create_app
|
|
from geniehive_control.models import HostHeartbeat, HostRegistration, RegisteredService, RoleProfile
|
|
from geniehive_control.registry import Registry
|
|
|
|
|
|
def test_registry_persists_registration_and_heartbeat(tmp_path: Path) -> None:
|
|
db_path = tmp_path / "geniehive.sqlite3"
|
|
registry = Registry(db_path)
|
|
|
|
host = registry.register_host(
|
|
HostRegistration(
|
|
host_id="atlas-01",
|
|
display_name="Atlas GPU Box",
|
|
address="192.168.1.101",
|
|
labels={"site": "home-lab"},
|
|
capabilities={"cuda": True},
|
|
resources={"cpu_threads": 24},
|
|
services=[
|
|
RegisteredService(
|
|
service_id="atlas-01/chat/qwen3-8b",
|
|
host_id="atlas-01",
|
|
kind="chat",
|
|
protocol="openai",
|
|
endpoint="http://192.168.1.101:18091",
|
|
runtime={"engine": "llama.cpp", "launcher": "managed"},
|
|
assets=[{"asset_id": "qwen3-8b-q4km", "loaded": True}],
|
|
state={"health": "healthy", "load_state": "loaded", "accept_requests": True},
|
|
observed={"p50_latency_ms": 900, "tokens_per_sec": 40},
|
|
)
|
|
],
|
|
)
|
|
)
|
|
assert host is not None
|
|
assert host["host_id"] == "atlas-01"
|
|
|
|
updated = registry.heartbeat_host(
|
|
HostHeartbeat(
|
|
host_id="atlas-01",
|
|
status={"state": "online"},
|
|
metrics={"gpu_utilization_pct": 77},
|
|
)
|
|
)
|
|
assert updated is not None
|
|
assert updated["metrics"]["gpu_utilization_pct"] == 77
|
|
|
|
hosts = registry.list_hosts()
|
|
services = registry.list_services()
|
|
health = registry.cluster_health(stale_after_s=30)
|
|
|
|
assert len(hosts) == 1
|
|
assert len(services) == 1
|
|
assert services[0]["service_id"] == "atlas-01/chat/qwen3-8b"
|
|
assert services[0]["state"]["health"] == "healthy"
|
|
assert health["host_count"] == 1
|
|
assert health["healthy_service_count"] == 1
|
|
|
|
|
|
def test_registry_persists_roles_and_resolves_direct_and_role_routes(tmp_path: Path) -> None:
|
|
db_path = tmp_path / "geniehive.sqlite3"
|
|
registry = Registry(db_path)
|
|
|
|
registry.register_host(
|
|
HostRegistration(
|
|
host_id="atlas-01",
|
|
address="192.168.1.101",
|
|
services=[
|
|
RegisteredService(
|
|
service_id="atlas-01/chat/qwen3-8b",
|
|
host_id="atlas-01",
|
|
kind="chat",
|
|
endpoint="http://192.168.1.101:18091",
|
|
assets=[{"asset_id": "qwen3-8b-q4km", "loaded": True}],
|
|
state={"health": "healthy", "load_state": "loaded", "accept_requests": True},
|
|
observed={"p50_latency_ms": 900},
|
|
),
|
|
RegisteredService(
|
|
service_id="atlas-01/embeddings/bge-small",
|
|
host_id="atlas-01",
|
|
kind="embeddings",
|
|
endpoint="http://192.168.1.101:18092",
|
|
assets=[{"asset_id": "bge-small-en", "loaded": True}],
|
|
state={"health": "healthy", "load_state": "loaded", "accept_requests": True},
|
|
observed={"p50_latency_ms": 120},
|
|
),
|
|
],
|
|
)
|
|
)
|
|
registry.upsert_roles(
|
|
[
|
|
RoleProfile(
|
|
role_id="mentor",
|
|
display_name="Mentor",
|
|
operation="chat",
|
|
modality="text",
|
|
routing_policy={"preferred_families": ["qwen3"]},
|
|
),
|
|
RoleProfile(
|
|
role_id="embedder",
|
|
display_name="Embedder",
|
|
operation="embeddings",
|
|
modality="text",
|
|
routing_policy={"require_loaded": True},
|
|
),
|
|
]
|
|
)
|
|
|
|
roles = registry.list_roles()
|
|
assert len(roles) == 2
|
|
assert roles[0]["role_id"] == "embedder"
|
|
|
|
direct = registry.resolve_route("qwen3-8b-q4km")
|
|
assert direct is not None
|
|
assert direct["match_type"] == "direct"
|
|
assert direct["service"]["service_id"] == "atlas-01/chat/qwen3-8b"
|
|
|
|
by_role = registry.resolve_route("mentor")
|
|
assert by_role is not None
|
|
assert by_role["match_type"] == "role"
|
|
assert by_role["role"]["role_id"] == "mentor"
|
|
assert by_role["service"]["service_id"] == "atlas-01/chat/qwen3-8b"
|
|
|
|
embed_role = registry.resolve_route("embedder")
|
|
assert embed_role is not None
|
|
assert embed_role["service"]["service_id"] == "atlas-01/embeddings/bge-small"
|
|
|
|
models = registry.list_client_models()
|
|
ids = {item["id"] for item in models}
|
|
assert "atlas-01/chat/qwen3-8b" in ids
|
|
assert "qwen3-8b-q4km" in ids
|
|
assert "mentor" in ids
|
|
mentor = next(item for item in models if item["id"] == "mentor")
|
|
assert mentor["geniehive"]["route_type"] == "role"
|
|
assert mentor["geniehive"]["offload_hint"]["suitability"] == "good_for_low_complexity"
|
|
asset = next(item for item in models if item["id"] == "qwen3-8b-q4km")
|
|
assert asset["geniehive"]["route_type"] == "asset"
|
|
assert asset["geniehive"]["offload_hint"]["recommended_for"] == "lower-complexity offload"
|
|
|
|
|
|
def test_control_app_exposes_expected_routes() -> None:
|
|
app = create_app()
|
|
paths = {route.path for route in app.routes}
|
|
assert "/health" in paths
|
|
assert "/v1/models" in paths
|
|
assert "/v1/nodes/register" in paths
|
|
assert "/v1/nodes/heartbeat" in paths
|
|
assert "/v1/cluster/hosts" in paths
|
|
assert "/v1/cluster/services" in paths
|
|
assert "/v1/cluster/roles" in paths
|
|
assert "/v1/cluster/health" in paths
|
|
assert "/v1/cluster/routes/resolve" in paths
|