cmvr_edge_ai/tests/unit/test_capability_registry.py

233 lines
7.0 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import pytest
from cmvr_edge_ai.capabilities import (
CapabilityDeploymentStatus,
CapabilityRegistry,
CapabilityRegistryError,
CapabilitySpec,
DuplicateCapabilityError,
ServingMode,
UnknownCapabilityError,
)
def _factory(params: dict[str, Any]) -> dict[str, Any]:
return dict(params)
def _spec(
model_id: str = "mobile-phone@1",
*,
category: str = "detect.mobile_phone",
serving_modes: tuple[ServingMode, ...] = (ServingMode.PASSIVE_INVOKE,),
) -> CapabilitySpec:
return CapabilitySpec(
category=category,
family="detect",
model_id=model_id,
serving_modes=serving_modes,
input_kinds=("image",),
output_kinds=("detections",),
artifact_roles=("annotated",),
parameters_schema={
"type": "object",
"properties": {"confidence": {"type": "number"}},
},
factory=_factory,
name="Mobile phone detector",
backend="onnxruntime",
)
def test_capability_spec_normalizes_and_defensively_copies_metadata() -> None:
schema: dict[str, Any] = {
"type": "object",
"required": ["confidence"],
}
spec = CapabilitySpec(
category="detect.analog_gauge",
family="detect",
model_id="analog-gauge-reader@1",
serving_modes=("active_push", "passive_invoke"),
input_kinds=("image",),
output_kinds=("scalar", "detections"),
artifact_roles=("annotated", "debug"),
parameters_schema=schema,
factory=_factory,
name=" Analog gauge reader ",
backend=" isolated-worker ",
)
schema["required"].append("unexpected")
assert spec.serving_modes == (
ServingMode.ACTIVE_PUSH,
ServingMode.PASSIVE_INVOKE,
)
assert spec.name == "Analog gauge reader"
assert spec.backend == "isolated-worker"
assert spec.parameters_schema["required"] == ["confidence"]
@pytest.mark.parametrize(
("overrides", "exception", "message"),
[
({"model_id": "mobile-phone"}, ValueError, "versioned"),
({"category": "talk.asr"}, ValueError, "must belong to family"),
({"category": "Detect.Phone"}, ValueError, "lowercase dotted"),
({"serving_modes": ()}, ValueError, "must not be empty"),
(
{"serving_modes": ("passive_invoke", "passive_invoke")},
ValueError,
"duplicates",
),
({"input_kinds": "image"}, TypeError, "ordered collection"),
({"output_kinds": ()}, ValueError, "must not be empty"),
({"factory": None}, TypeError, "factory must be callable"),
(
{"parameters_schema": {"default": object()}},
ValueError,
"only JSON values",
),
],
)
def test_capability_spec_rejects_invalid_metadata(
overrides: dict[str, Any], exception: type[Exception], message: str
) -> None:
values: dict[str, Any] = {
"category": "detect.mobile_phone",
"family": "detect",
"model_id": "mobile-phone@1",
"serving_modes": ("passive_invoke",),
"input_kinds": ("image",),
"output_kinds": ("detections",),
"factory": _factory,
}
values.update(overrides)
with pytest.raises(exception, match=message):
CapabilitySpec(**values)
def test_registry_registers_resolves_filters_and_creates() -> None:
registry = CapabilityRegistry()
phone = registry.register(_spec("phone@1"))
gauge = registry.register(
_spec(
"gauge@2",
category="detect.analog_gauge",
serving_modes=(ServingMode.ACTIVE_PUSH, ServingMode.PASSIVE_INVOKE),
)
)
assert registry.resolve("phone@1") is phone
assert registry.specs() == (gauge, phone)
assert registry.specs_for_category(
"detect.analog_gauge", serving_mode="active_push"
) == (gauge,)
assert registry.create("phone@1", {"confidence": 0.6}) == {"confidence": 0.6}
def test_registry_rejects_duplicates_and_reports_available_capabilities() -> None:
registry = CapabilityRegistry()
registry.register(_spec("phone@1"))
with pytest.raises(DuplicateCapabilityError, match="phone@1"):
registry.register(_spec("phone@1"))
with pytest.raises(
UnknownCapabilityError, match=r"available capabilities: phone@1"
):
registry.resolve("missing@1")
def test_catalog_groups_modes_and_keeps_registration_and_deployment_separate() -> None:
registry = CapabilityRegistry()
dual = registry.register(
_spec(
"gauge@1",
category="detect.analog_gauge",
serving_modes=(ServingMode.ACTIVE_PUSH, ServingMode.PASSIVE_INVOKE),
)
)
passive = registry.register(_spec("phone@1"))
catalog = registry.build_catalog(
{
dual.model_id: CapabilityDeploymentStatus.READY,
passive.model_id: CapabilityDeploymentStatus.DISABLED,
}
)
assert [item.model_id for item in catalog.active_push] == ["gauge@1"]
assert [item.model_id for item in catalog.passive_invoke] == [
"gauge@1",
"phone@1",
]
assert catalog.active_push[0].registration_status.value == "registered"
assert (
catalog.active_push[0].deployment_status
is CapabilityDeploymentStatus.READY
)
assert (
catalog.passive_invoke[1].deployment_status
is CapabilityDeploymentStatus.DISABLED
)
assert catalog.model_dump(mode="json")["schema_version"] == (
"cmvr.model-catalog/v1"
)
with pytest.raises(UnknownCapabilityError, match="unknown capability"):
registry.build_catalog({"missing@1": CapabilityDeploymentStatus.READY})
@dataclass
class _EntryPoint:
name: str
value: Any
def load(self) -> Any:
return self.value
class _EntryPoints(list[_EntryPoint]):
def select(self, *, group: str) -> _EntryPoints:
assert group == CapabilityRegistry.ENTRY_POINT_GROUP
return self
def test_registry_discovers_specs_and_callbacks(
monkeypatch,
) -> None: # type: ignore[no-untyped-def]
def callback(registry: CapabilityRegistry) -> None:
registry.register(_spec("callback@1"))
monkeypatch.setattr(
"cmvr_edge_ai.capabilities.registry.metadata.entry_points",
lambda: _EntryPoints(
[
_EntryPoint("spec", _spec("spec@1")),
_EntryPoint("callback", callback),
]
),
)
registry = CapabilityRegistry()
registry.load_entry_points()
assert [spec.model_id for spec in registry.specs()] == ["callback@1", "spec@1"]
def test_registry_rejects_invalid_entry_point(
monkeypatch,
) -> None: # type: ignore[no-untyped-def]
monkeypatch.setattr(
"cmvr_edge_ai.capabilities.registry.metadata.entry_points",
lambda: _EntryPoints([_EntryPoint("invalid", object())]),
)
with pytest.raises(CapabilityRegistryError, match="invalid"):
CapabilityRegistry().load_entry_points()