mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-14 09:39:11 +02:00
134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
"""Tests for the Skills plugin wrapper.
|
|
|
|
Provider behavior (skill discovery, file exposure, etc.) is covered by
|
|
`test_skills_provider.py` and `test_skills_vendor_providers.py`. This
|
|
file only covers plugin-layer concerns — config validation, meta,
|
|
vendor→path resolution, and the deprecation shim at the old import path.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import warnings
|
|
from pathlib import Path
|
|
from typing import cast
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from fastmcp.server.plugins.skills import Skills, SkillsConfig
|
|
from fastmcp.server.plugins.skills.directory_provider import SkillsDirectoryProvider
|
|
from fastmcp.server.plugins.skills.plugin import VENDOR_PATHS, Vendor
|
|
from fastmcp.server.plugins.skills.skill_provider import SkillProvider
|
|
|
|
|
|
class TestSkillsConfig:
|
|
def test_config_generic_binding(self):
|
|
assert Skills._config_cls is SkillsConfig
|
|
assert Skills.Config is SkillsConfig
|
|
|
|
def test_default_config_instantiable(self):
|
|
"""Defaults must pass the plugin framework's instantiate-with-no-args
|
|
contract; the source check fires at providers() time."""
|
|
assert Skills.Config() # must not raise
|
|
|
|
def test_unknown_config_key_rejected(self):
|
|
with pytest.raises((ValidationError, Exception), match="forbid|extra"):
|
|
Skills.Config(not_a_real_option=True) # ty: ignore[unknown-argument]
|
|
|
|
def test_default_meta(self):
|
|
assert Skills.meta.name == "skills"
|
|
assert Skills.meta.version is None
|
|
|
|
|
|
class TestSourceResolution:
|
|
def test_path_source_builds_skill_provider(self, tmp_path: Path):
|
|
skill = tmp_path / "my-skill"
|
|
skill.mkdir()
|
|
(skill / "SKILL.md").write_text("# My Skill")
|
|
|
|
plugin = Skills(Skills.Config(path=str(skill)))
|
|
providers = plugin.providers()
|
|
assert isinstance(providers[0], SkillProvider)
|
|
|
|
def test_directory_source_builds_directory_provider(self, tmp_path: Path):
|
|
plugin = Skills(Skills.Config(directory=str(tmp_path)))
|
|
providers = plugin.providers()
|
|
assert isinstance(providers[0], SkillsDirectoryProvider)
|
|
|
|
def test_directory_source_accepts_list(self, tmp_path: Path):
|
|
a, b = tmp_path / "a", tmp_path / "b"
|
|
a.mkdir()
|
|
b.mkdir()
|
|
plugin = Skills(Skills.Config(directory=[str(a), str(b)]))
|
|
providers = plugin.providers()
|
|
assert isinstance(providers[0], SkillsDirectoryProvider)
|
|
|
|
@pytest.mark.parametrize("vendor", list(VENDOR_PATHS))
|
|
def test_vendor_presets_resolve_to_known_paths(self, vendor: str):
|
|
"""Every vendor string must produce a directory provider rooted
|
|
at the paths the old vendor subclass used to hardcode."""
|
|
plugin = Skills(Skills.Config(vendor=cast(Vendor, vendor)))
|
|
providers = plugin.providers()
|
|
assert isinstance(providers[0], SkillsDirectoryProvider)
|
|
|
|
def test_no_source_fails_at_build_time(self):
|
|
plugin = Skills(Skills.Config())
|
|
with pytest.raises(ValueError, match="path.*directory.*vendor"):
|
|
plugin.providers()
|
|
|
|
def test_multiple_sources_rejected(self, tmp_path: Path):
|
|
plugin = Skills(Skills.Config(directory=str(tmp_path), vendor="claude"))
|
|
with pytest.raises(ValueError, match="exactly one"):
|
|
plugin.providers()
|
|
|
|
|
|
class TestDeprecationShim:
|
|
"""The old `fastmcp.server.providers.skills` package shims back to the
|
|
new plugin package. Top-level stays silent; leaf submodule imports
|
|
emit `FastMCPDeprecationWarning`."""
|
|
|
|
def test_top_level_is_silent(self):
|
|
import importlib
|
|
import sys
|
|
|
|
from fastmcp.exceptions import FastMCPDeprecationWarning
|
|
|
|
sys.modules.pop("fastmcp.server.providers.skills", None)
|
|
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
warnings.simplefilter("always")
|
|
importlib.import_module("fastmcp.server.providers.skills")
|
|
|
|
fastmcp_warns = [
|
|
w for w in caught if issubclass(w.category, FastMCPDeprecationWarning)
|
|
]
|
|
assert not fastmcp_warns
|
|
|
|
def test_leaf_submodule_import_emits_deprecation_warning(self):
|
|
import importlib
|
|
import sys
|
|
|
|
from fastmcp.exceptions import FastMCPDeprecationWarning
|
|
|
|
sys.modules.pop("fastmcp.server.providers.skills.vendor_providers", None)
|
|
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
warnings.simplefilter("always")
|
|
importlib.import_module("fastmcp.server.providers.skills.vendor_providers")
|
|
|
|
fastmcp_warns = [
|
|
w for w in caught if issubclass(w.category, FastMCPDeprecationWarning)
|
|
]
|
|
assert any("plugins.skills" in str(w.message) for w in fastmcp_warns)
|
|
|
|
def test_old_import_path_symbols_still_resolve(self):
|
|
"""`ClaudeSkillsProvider` and friends keep resolving through the
|
|
silent package-level shim."""
|
|
from fastmcp.server.plugins.skills.claude_provider import (
|
|
ClaudeSkillsProvider as NewClass,
|
|
)
|
|
from fastmcp.server.providers.skills import (
|
|
ClaudeSkillsProvider as OldClass,
|
|
)
|
|
|
|
assert OldClass is NewClass
|