nanobot/tests/extensions/test_registry.py

322 lines
8.5 KiB
Python

from nanobot.extensions import (
ContributionKind,
DependencyKind,
ExtensionCandidate,
ExtensionContribution,
ExtensionDependency,
ExtensionManifest,
ExtensionPermission,
ExtensionPolicy,
ExtensionRegistry,
ExtensionRuntime,
ExtensionScope,
)
def _candidate(
extension_id: str,
*,
scope: ExtensionScope,
contribution_name: str = "",
trusted: bool = True,
dependencies: tuple[ExtensionDependency, ...] = (),
) -> ExtensionCandidate:
contributions = (
ExtensionContribution(
kind=ContributionKind.TOOL,
name=contribution_name,
),
) if contribution_name else ()
return ExtensionCandidate(
manifest=ExtensionManifest(
id=extension_id,
name=extension_id,
version="1.0.0",
runtime=ExtensionRuntime.PYTHON,
contributions=contributions,
dependencies=dependencies,
),
scope=scope,
trusted=trusted,
)
def test_workspace_copy_shadows_user_and_builtin_copy_of_same_extension() -> None:
registry = ExtensionRegistry()
registry.register(_candidate("acme", scope=ExtensionScope.BUILTIN))
registry.register(_candidate("acme", scope=ExtensionScope.USER))
registry.register(_candidate("acme", scope=ExtensionScope.WORKSPACE))
snapshot = registry.snapshot()
assert len(snapshot.extensions) == 1
assert snapshot.extensions[0].scope is ExtensionScope.WORKSPACE
def test_policy_filters_extensions_before_contribution_resolution() -> None:
registry = ExtensionRegistry(
ExtensionPolicy(allow=frozenset({"allowed"}), deny=frozenset())
)
registry.register(
_candidate(
"allowed",
scope=ExtensionScope.USER,
contribution_name="allowed_tool",
)
)
registry.register(
_candidate(
"hidden",
scope=ExtensionScope.USER,
contribution_name="hidden_tool",
)
)
snapshot = registry.snapshot()
assert [extension.manifest.id for extension in snapshot.extensions] == ["allowed"]
assert [
contribution.contribution.name for contribution in snapshot.contributions
] == ["allowed_tool"]
def test_untrusted_external_extension_is_visible_to_discovery_but_not_active() -> None:
registry = ExtensionRegistry()
registry.register(
_candidate(
"untrusted",
scope=ExtensionScope.USER,
contribution_name="unsafe_tool",
trusted=False,
)
)
snapshot = registry.snapshot()
assert snapshot.extensions == ()
assert snapshot.contributions == ()
def test_invalid_package_integrity_cannot_be_overridden_by_trust() -> None:
registry = ExtensionRegistry()
candidate = _candidate(
"tampered",
scope=ExtensionScope.USER,
contribution_name="unsafe_tool",
)
registry.register(
ExtensionCandidate(
manifest=candidate.manifest,
scope=candidate.scope,
trusted=True,
integrity_valid=False,
)
)
snapshot = registry.snapshot()
assert snapshot.extensions == ()
assert snapshot.contributions == ()
def test_external_extension_requires_every_requested_permission() -> None:
candidate = ExtensionCandidate(
manifest=ExtensionManifest(
id="permission.test",
name="Permission test",
version="1.0.0",
runtime=ExtensionRuntime.DECLARATIVE,
permissions=(
ExtensionPermission(name="network", reason="Fetch data."),
ExtensionPermission(
name="filesystem.read",
reason="Read input.",
),
),
),
scope=ExtensionScope.USER,
trusted=True,
granted_permissions=frozenset({"network"}),
)
registry = ExtensionRegistry()
registry.register(candidate)
snapshot = registry.snapshot()
assert snapshot.extensions == ()
assert snapshot.diagnostics[0].code == "permission_required"
assert "filesystem.read" in snapshot.diagnostics[0].message
def test_conflicting_contribution_does_not_silently_replace_owner() -> None:
registry = ExtensionRegistry()
registry.register(
_candidate(
"core",
scope=ExtensionScope.BUILTIN,
contribution_name="shell",
)
)
registry.register(
_candidate(
"third-party",
scope=ExtensionScope.WORKSPACE,
contribution_name="shell",
)
)
snapshot = registry.snapshot()
assert snapshot.contributions[0].owner.manifest.id == "core"
assert snapshot.diagnostics[0].code == "contribution_conflict"
def test_higher_scope_extension_cannot_replace_another_owner() -> None:
registry = ExtensionRegistry()
registry.register(
_candidate(
"core",
scope=ExtensionScope.BUILTIN,
contribution_name="shell",
)
)
registry.register(
_candidate(
"replacement",
scope=ExtensionScope.WORKSPACE,
contribution_name="shell",
)
)
snapshot = registry.snapshot()
assert snapshot.contributions[0].owner.manifest.id == "core"
assert snapshot.diagnostics[0].code == "contribution_conflict"
def test_extension_dependency_must_be_active_and_starts_first() -> None:
dependency = ExtensionDependency(
kind=DependencyKind.EXTENSION,
name="z-base",
)
registry = ExtensionRegistry()
registry.register(
_candidate(
"a-dependent",
scope=ExtensionScope.USER,
dependencies=(dependency,),
)
)
registry.register(_candidate("z-base", scope=ExtensionScope.USER))
snapshot = registry.snapshot()
assert [item.manifest.id for item in snapshot.extensions] == [
"z-base",
"a-dependent",
]
def test_inactive_extension_cannot_satisfy_dependency() -> None:
dependency = ExtensionDependency(
kind=DependencyKind.EXTENSION,
name="base",
)
registry = ExtensionRegistry()
registry.register(
_candidate(
"dependent",
scope=ExtensionScope.USER,
dependencies=(dependency,),
)
)
registry.register(
_candidate("base", scope=ExtensionScope.USER, trusted=False)
)
snapshot = registry.snapshot()
assert snapshot.extensions == ()
assert any(
item.code == "dependency_missing"
and item.extension_id == "dependent"
for item in snapshot.diagnostics
)
def test_extension_dependency_cycle_is_rejected() -> None:
registry = ExtensionRegistry()
registry.register(
_candidate(
"first",
scope=ExtensionScope.USER,
dependencies=(
ExtensionDependency(
kind=DependencyKind.EXTENSION,
name="second",
),
),
)
)
registry.register(
_candidate(
"second",
scope=ExtensionScope.USER,
dependencies=(
ExtensionDependency(
kind=DependencyKind.EXTENSION,
name="first",
),
),
)
)
snapshot = registry.snapshot()
assert snapshot.extensions == ()
assert {
item.extension_id
for item in snapshot.diagnostics
if item.code == "dependency_cycle"
} == {"first", "second"}
def test_optional_extension_dependency_cycle_is_allowed() -> None:
registry = ExtensionRegistry()
registry.register(
_candidate(
"first",
scope=ExtensionScope.USER,
dependencies=(
ExtensionDependency(
kind=DependencyKind.EXTENSION,
name="second",
optional=True,
),
),
)
)
registry.register(
_candidate(
"second",
scope=ExtensionScope.USER,
dependencies=(
ExtensionDependency(
kind=DependencyKind.EXTENSION,
name="first",
optional=True,
),
),
)
)
snapshot = registry.snapshot()
assert {item.manifest.id for item in snapshot.extensions} == {
"first",
"second",
}
assert not any(
item.code == "dependency_cycle" for item in snapshot.diagnostics
)