|
| 1 | +""" |
| 2 | +Tests for the unified PluginSpec contract (R2 of plugin refactor). |
| 3 | +
|
| 4 | +These tests lock in: |
| 5 | + * the dataclass shape (required vs default fields, sensible defaults) |
| 6 | + * back-compat aliases (ServiceSpec / ComponentSpec subclass behaviour, |
| 7 | + requires / recommends read-only properties) |
| 8 | + * registry-shape guarantees (every in-tree spec is a PluginSpec with |
| 9 | + a sane kind and verified=True). |
| 10 | +""" |
| 11 | + |
| 12 | +import pytest |
| 13 | + |
| 14 | +from aegis.core.components import COMPONENTS, ComponentSpec, ComponentType |
| 15 | +from aegis.core.file_manifest import FileManifest |
| 16 | +from aegis.core.plugin_spec import PluginKind, PluginSpec |
| 17 | +from aegis.core.services import SERVICES, ServiceSpec, ServiceType |
| 18 | + |
| 19 | + |
| 20 | +class TestPluginSpec: |
| 21 | + """The unified dataclass itself.""" |
| 22 | + |
| 23 | + def test_minimal_construction(self) -> None: |
| 24 | + spec = PluginSpec(name="x", kind=PluginKind.SERVICE, description="d") |
| 25 | + assert spec.name == "x" |
| 26 | + assert spec.kind is PluginKind.SERVICE |
| 27 | + assert spec.description == "d" |
| 28 | + assert spec.type is None |
| 29 | + assert spec.required_components == [] |
| 30 | + assert spec.recommended_components == [] |
| 31 | + assert spec.required_services == [] |
| 32 | + assert spec.required_plugins == [] |
| 33 | + assert spec.conflicts == [] |
| 34 | + assert spec.pyproject_deps == [] |
| 35 | + assert spec.docker_services == [] |
| 36 | + assert spec.template_files == [] |
| 37 | + assert isinstance(spec.files, FileManifest) |
| 38 | + assert spec.version == "0.0.0" |
| 39 | + assert spec.verified is True |
| 40 | + |
| 41 | + def test_kind_is_required(self) -> None: |
| 42 | + with pytest.raises(TypeError): |
| 43 | + PluginSpec(name="x", description="d") # type: ignore[call-arg] |
| 44 | + |
| 45 | + def test_independent_default_lists(self) -> None: |
| 46 | + a = PluginSpec(name="a", kind=PluginKind.SERVICE, description="") |
| 47 | + b = PluginSpec(name="b", kind=PluginKind.SERVICE, description="") |
| 48 | + a.required_components.append("redis") |
| 49 | + assert b.required_components == [] |
| 50 | + |
| 51 | + def test_requires_alias_reads_required_components(self) -> None: |
| 52 | + spec = PluginSpec( |
| 53 | + name="x", |
| 54 | + kind=PluginKind.COMPONENT, |
| 55 | + description="d", |
| 56 | + required_components=["redis", "database"], |
| 57 | + ) |
| 58 | + assert spec.requires == ["redis", "database"] |
| 59 | + assert spec.requires is spec.required_components # same list |
| 60 | + |
| 61 | + def test_recommends_alias_reads_recommended_components(self) -> None: |
| 62 | + spec = PluginSpec( |
| 63 | + name="x", |
| 64 | + kind=PluginKind.COMPONENT, |
| 65 | + description="d", |
| 66 | + recommended_components=["backend"], |
| 67 | + ) |
| 68 | + assert spec.recommends == ["backend"] |
| 69 | + assert spec.recommends is spec.recommended_components |
| 70 | + |
| 71 | + def test_unverified_third_party_default_pattern(self) -> None: |
| 72 | + """A third-party plugin would set ``verified=False`` explicitly.""" |
| 73 | + spec = PluginSpec( |
| 74 | + name="aegis-plugin-scraper", |
| 75 | + kind=PluginKind.SERVICE, |
| 76 | + description="Web scraping", |
| 77 | + verified=False, |
| 78 | + version="0.1.0", |
| 79 | + required_plugins=["auth>=1.0"], |
| 80 | + ) |
| 81 | + assert spec.verified is False |
| 82 | + assert spec.version == "0.1.0" |
| 83 | + assert spec.required_plugins == ["auth>=1.0"] |
| 84 | + |
| 85 | + |
| 86 | +class TestSubclassAliases: |
| 87 | + """ServiceSpec and ComponentSpec are PluginSpec subclasses with a |
| 88 | + pre-baked ``kind`` default. Pre-R2 call sites rely on this. |
| 89 | + """ |
| 90 | + |
| 91 | + def test_servicespec_defaults_kind_service(self) -> None: |
| 92 | + spec = ServiceSpec(name="x", type=ServiceType.AUTH, description="d") |
| 93 | + assert spec.kind is PluginKind.SERVICE |
| 94 | + assert isinstance(spec, PluginSpec) |
| 95 | + assert isinstance(spec, ServiceSpec) |
| 96 | + |
| 97 | + def test_componentspec_defaults_kind_component(self) -> None: |
| 98 | + spec = ComponentSpec( |
| 99 | + name="x", type=ComponentType.INFRASTRUCTURE, description="d" |
| 100 | + ) |
| 101 | + assert spec.kind is PluginKind.COMPONENT |
| 102 | + assert isinstance(spec, PluginSpec) |
| 103 | + assert isinstance(spec, ComponentSpec) |
| 104 | + |
| 105 | + def test_servicespec_can_override_kind(self) -> None: |
| 106 | + """Just because — ensures the default is overridable, not pinned.""" |
| 107 | + spec = ServiceSpec( |
| 108 | + name="x", |
| 109 | + kind=PluginKind.COMPONENT, |
| 110 | + type=ServiceType.AUTH, |
| 111 | + description="d", |
| 112 | + ) |
| 113 | + assert spec.kind is PluginKind.COMPONENT |
| 114 | + |
| 115 | + |
| 116 | +class TestRegistryShape: |
| 117 | + """In-tree services and components are first-party plugins.""" |
| 118 | + |
| 119 | + def test_every_service_is_first_party(self) -> None: |
| 120 | + for name, spec in SERVICES.items(): |
| 121 | + assert isinstance(spec, ServiceSpec), f"{name} not a ServiceSpec" |
| 122 | + assert isinstance(spec, PluginSpec) |
| 123 | + assert spec.kind is PluginKind.SERVICE, name |
| 124 | + assert spec.verified is True, f"{name} should be verified=True" |
| 125 | + assert isinstance(spec.type, ServiceType), f"{name} type wrong" |
| 126 | + |
| 127 | + def test_every_component_is_first_party(self) -> None: |
| 128 | + for name, spec in COMPONENTS.items(): |
| 129 | + assert isinstance(spec, ComponentSpec), f"{name} not a ComponentSpec" |
| 130 | + assert isinstance(spec, PluginSpec) |
| 131 | + assert spec.kind is PluginKind.COMPONENT, name |
| 132 | + assert spec.verified is True, f"{name} should be verified=True" |
| 133 | + assert isinstance(spec.type, ComponentType), f"{name} type wrong" |
| 134 | + |
| 135 | + def test_no_servicespec_in_components(self) -> None: |
| 136 | + """ComponentSpec instances are NOT ServiceSpec instances (sibling subclasses).""" |
| 137 | + for name, spec in COMPONENTS.items(): |
| 138 | + assert not isinstance(spec, ServiceSpec), ( |
| 139 | + f"component {name} leaked as ServiceSpec" |
| 140 | + ) |
| 141 | + |
| 142 | + def test_no_componentspec_in_services(self) -> None: |
| 143 | + for name, spec in SERVICES.items(): |
| 144 | + assert not isinstance(spec, ComponentSpec), ( |
| 145 | + f"service {name} leaked as ComponentSpec" |
| 146 | + ) |
0 commit comments