nanobot/tests/extensions/test_native.py

152 lines
4.8 KiB
Python

import sys
from unittest.mock import patch
from nanobot.agent.skills import SkillsLoader
from nanobot.agent.tools.base import Tool
from nanobot.agent.tools.registry import ToolRegistry
from nanobot.command.router import CommandRouter
from nanobot.config.schema import Config, MCPServerConfig
from nanobot.extensions import (
ContributionKind,
ExtensionRuntime,
discover_native_extensions,
)
from nanobot.extensions.native import _python_dependencies
class _Tool(Tool):
@property
def name(self) -> str:
return "acme_tool"
@property
def description(self) -> str:
return "Acme tool"
@property
def parameters(self) -> dict:
return {"type": "object"}
async def execute(self, **kwargs):
return kwargs
def test_native_inventory_preserves_runtime_ownership(tmp_path) -> None:
tools = ToolRegistry()
tools.register(_Tool(), owner="acme.extension")
commands = CommandRouter()
async def _handler(_ctx):
return None
commands.exact("/acme", _handler, owner="acme.extension")
config = Config()
config.tools.mcp_servers["docs"] = MCPServerConfig(url="https://example.com")
with (
patch("nanobot.channels.registry.discover_plugins", return_value={}),
patch("nanobot.providers.registry.PROVIDERS", ()),
patch("nanobot.audio.transcription_registry.TRANSCRIPTION_PROVIDERS", ()),
patch(
"nanobot.providers.image_generation.image_gen_provider_names",
return_value=(),
),
):
result = discover_native_extensions(
config,
skills=SkillsLoader(tmp_path, builtin_skills_dir=tmp_path / "missing"),
tools=tools,
commands=commands,
)
extensions = {item.manifest.id: item for item in result.candidates}
assert {
contribution.kind
for contribution in extensions["acme.extension"].manifest.contributions
} == {ContributionKind.TOOL, ContributionKind.COMMAND}
assert extensions["acme.extension"].manifest.runtime is ExtensionRuntime.DECLARATIVE
assert "nanobot.mcp.docs" in extensions
def test_native_inventory_projects_workspace_skill(tmp_path) -> None:
skill_dir = tmp_path / "skills" / "release"
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text(
"---\n"
"name: release\n"
"description: Prepare a release.\n"
"metadata:\n"
" nanobot:\n"
" requires:\n"
" bins: [gh]\n"
"---\n"
)
with (
patch("nanobot.channels.registry.discover_plugins", return_value={}),
patch("nanobot.providers.registry.PROVIDERS", ()),
patch("nanobot.audio.transcription_registry.TRANSCRIPTION_PROVIDERS", ()),
patch(
"nanobot.providers.image_generation.image_gen_provider_names",
return_value=(),
),
):
result = discover_native_extensions(
Config(),
skills=SkillsLoader(tmp_path, builtin_skills_dir=tmp_path / "missing"),
)
skill = next(
item for item in result.candidates if item.manifest.id == "nanobot.skill.release"
)
assert skill.scope.name == "WORKSPACE"
assert skill.manifest.runtime is ExtensionRuntime.DECLARATIVE
assert skill.manifest.contributions[0].description == "Prepare a release."
def test_native_inventory_collapses_command_routing_tiers() -> None:
commands = CommandRouter()
async def _handler(_ctx):
return None
commands.priority("/status", _handler)
commands.exact("/status", _handler)
commands.exact("/model", _handler)
commands.prefix("/model ", _handler)
with (
patch("nanobot.channels.registry.discover_plugins", return_value={}),
patch("nanobot.providers.registry.PROVIDERS", ()),
patch("nanobot.audio.transcription_registry.TRANSCRIPTION_PROVIDERS", ()),
patch(
"nanobot.providers.image_generation.image_gen_provider_names",
return_value=(),
),
):
result = discover_native_extensions(Config(), commands=commands)
core = next(
item for item in result.candidates if item.manifest.id == "nanobot.core"
)
assert [
contribution.name for contribution in core.manifest.contributions
] == ["model", "status"]
def test_python_dependencies_project_distribution_identity_and_active_markers() -> None:
dependencies = _python_dependencies(
(
"httpx[http2]>=0.27",
f"platform-package>=1; sys_platform == '{sys.platform}'",
"inactive-package>=1; sys_platform == '__never__'",
"packaging>=24; python_version >= '3.11'",
)
)
assert [(item.name, item.specifier) for item in dependencies] == [
("httpx", ">=0.27"),
("platform-package", ">=1"),
("packaging", ">=24"),
]