big rework
This commit is contained in:
@@ -1 +0,0 @@
|
||||
"""Test package marker for intra-suite imports."""
|
||||
@@ -1,3 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# Global lightweight fixtures can be added here as the suite grows.
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.prompts.content import load_prompt_definition
|
||||
from personal_mcp.prompts.content import render_prompt
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
class TestPromptContentRenderer:
|
||||
def test_renders_arguments_without_frontmatter(self) -> None:
|
||||
rendered = render_prompt(
|
||||
"jsfiddle-page-layout",
|
||||
{"domain": "public library", "layout_brief": None},
|
||||
)
|
||||
|
||||
assert not rendered.startswith("---")
|
||||
assert "`domain`: public library" in rendered
|
||||
assert "`layout_brief`: Not provided" in rendered
|
||||
|
||||
def test_rejects_placeholder_drift(self) -> None:
|
||||
with pytest.raises(ValueError, match="placeholders do not match arguments"):
|
||||
render_prompt("jsfiddle-page-layout", {"domain": "public library"})
|
||||
|
||||
def test_rejects_invalid_prompt_id(self) -> None:
|
||||
with pytest.raises(ValueError, match="lowercase kebab-case"):
|
||||
render_prompt("../outside", {})
|
||||
|
||||
def test_rejects_missing_prompt_metadata(self, tmp_path: Path) -> None:
|
||||
document = tmp_path / "PROMPT.md"
|
||||
document.write_text("---\nicon: lucide/messages-square\n---\n\n# Body\n", encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError, match="missing the 'prompt' block"):
|
||||
load_prompt_definition("demo", document)
|
||||
|
||||
def test_rejects_unknown_prompt_metadata(self, tmp_path: Path) -> None:
|
||||
document = tmp_path / "PROMPT.md"
|
||||
document.write_text(
|
||||
"---\nprompt: {version: '1', description: Demo, tags: [demo], arguments: {}, unknown: true}\n"
|
||||
"---\n\n# Body\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Extra inputs are not permitted"):
|
||||
load_prompt_definition("demo", document)
|
||||
|
||||
def test_rejects_declared_placeholder_drift(self, tmp_path: Path) -> None:
|
||||
document = tmp_path / "PROMPT.md"
|
||||
document.write_text(
|
||||
"---\nprompt: {version: '1', description: Demo, tags: [demo], arguments: "
|
||||
"{topic: {description: Topic, required: true}}}\n---\n\n# Body\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="placeholders do not match arguments"):
|
||||
load_prompt_definition("demo", document)
|
||||
@@ -1,121 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastmcp import Client
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.exceptions import PromptError
|
||||
|
||||
from personal_mcp.prompts import create_prompts_provider
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
EXPECTED_PROMPTS = {
|
||||
"authoring",
|
||||
"greenfield-architecture",
|
||||
"jsfiddle-page-layout",
|
||||
"mcp-consumer-repo-shim",
|
||||
"nicegui-component-extraction",
|
||||
"pytest-fill-scaffold",
|
||||
"pytest-scaffold",
|
||||
}
|
||||
|
||||
|
||||
def write_prompt(document: Path, *, description: str, heading: str = "Demo") -> None:
|
||||
document.parent.mkdir(parents=True, exist_ok=True)
|
||||
document.write_text(
|
||||
"---\n"
|
||||
f"prompt: {{version: '1.0.0', description: {description!r}, tags: [demo], arguments: "
|
||||
"{kind: {description: 'Kind to render.', required: true, choices: [first, second]}, "
|
||||
"note: {description: 'Optional note.', required: false}}}\n"
|
||||
"---\n\n"
|
||||
f"# {heading}\n\nKind: {{{{kind}}}}\n\nNote: {{{{note}}}}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
class TestMarkdownPromptsProvider:
|
||||
@pytest.mark.asyncio
|
||||
async def test_discovers_exact_authored_set(self) -> None:
|
||||
mcp = FastMCP("prompts-test")
|
||||
mcp.add_provider(create_prompts_provider())
|
||||
|
||||
async with Client(mcp) as client:
|
||||
prompts = await client.list_prompts()
|
||||
|
||||
assert {prompt.name for prompt in prompts} == EXPECTED_PROMPTS
|
||||
assert all(prompt.description for prompt in prompts)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exposes_typed_arguments_and_renders_markdown(self) -> None:
|
||||
mcp = FastMCP("prompts-test")
|
||||
mcp.add_provider(create_prompts_provider())
|
||||
|
||||
async with Client(mcp) as client:
|
||||
prompts = await client.list_prompts()
|
||||
authoring = next(prompt for prompt in prompts if prompt.name == "authoring")
|
||||
result = await client.get_prompt(
|
||||
"authoring",
|
||||
{
|
||||
"artifact_type": "skill",
|
||||
"artifact_id": "demo-skill",
|
||||
"goal": "Demonstrate typed prompts.",
|
||||
},
|
||||
)
|
||||
|
||||
required = {argument.name for argument in authoring.arguments or [] if argument.required}
|
||||
artifact_type = next(argument for argument in authoring.arguments or [] if argument.name == "artifact_type")
|
||||
assert required == {"artifact_type", "artifact_id", "goal"}
|
||||
assert artifact_type.description == "Artifact type to create.\n\nAccepted values: skill, prompt, shim."
|
||||
assert result.messages
|
||||
assert "`artifact_id`: demo-skill" in result.messages[0].content.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enforces_required_arguments_and_choices(self) -> None:
|
||||
provider = create_prompts_provider()
|
||||
prompt = await provider.get_prompt("authoring")
|
||||
|
||||
assert prompt is not None
|
||||
with pytest.raises(PromptError, match="Missing required arguments"):
|
||||
await prompt.render({"artifact_type": "skill"})
|
||||
with pytest.raises(PromptError, match="must be one of"):
|
||||
await prompt.render(
|
||||
{
|
||||
"artifact_type": "unsupported",
|
||||
"artifact_id": "demo-skill",
|
||||
"goal": "Demonstrate validation.",
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_live_loads_edits_without_python_components(self, tmp_path: Path) -> None:
|
||||
prompts_root = tmp_path / "prompts"
|
||||
prompts_root.mkdir()
|
||||
provider = create_prompts_provider(prompts_root)
|
||||
mcp = FastMCP("prompts-test")
|
||||
mcp.add_provider(provider)
|
||||
|
||||
async with Client(mcp) as client:
|
||||
assert await client.list_prompts() == []
|
||||
|
||||
document = prompts_root / "dynamic-demo" / "PROMPT.md"
|
||||
write_prompt(document, description="Initial description")
|
||||
|
||||
prompts = await client.list_prompts()
|
||||
assert [prompt.name for prompt in prompts] == ["dynamic-demo"]
|
||||
assert prompts[0].description == "Initial description"
|
||||
result = await client.get_prompt("dynamic-demo", {"kind": "first"})
|
||||
assert "# Demo" in result.messages[0].content.text
|
||||
assert "Note: Not provided" in result.messages[0].content.text
|
||||
|
||||
write_prompt(document, description="Updated description", heading="Updated")
|
||||
|
||||
prompts = await client.list_prompts()
|
||||
assert prompts[0].description == "Updated description"
|
||||
result = await client.get_prompt("dynamic-demo", {"kind": "second", "note": "ready"})
|
||||
assert "# Updated" in result.messages[0].content.text
|
||||
assert "Note: ready" in result.messages[0].content.text
|
||||
|
||||
document.unlink()
|
||||
document.parent.rmdir()
|
||||
|
||||
assert await client.list_prompts() == []
|
||||
@@ -1,21 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.registry.load import get_docs_registry
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
class TestCurrentDocsIngestion:
|
||||
"""Covers ingestion of the repository's current docs tree."""
|
||||
|
||||
def test_registry_includes_docs_and_excludes_skills(self) -> None:
|
||||
"""Ensures the docs registry cannot duplicate native skill resources."""
|
||||
registry = get_docs_registry()
|
||||
|
||||
assert PurePosixPath("index.md") in registry.docs_markdown_by_path
|
||||
assert any(path.parts[0] == "prompts" for path in registry.docs_markdown_by_path)
|
||||
assert all(path.parts[0] != "skills" for path in registry.docs_markdown_by_path)
|
||||
@@ -1,33 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.registry.models import parse_docs_path
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
class TestDocsPathValidation:
|
||||
"""Covers canonical resource-path contracts."""
|
||||
|
||||
def test_parse_docs_path_returns_pure_posix_path(self) -> None:
|
||||
path = parse_docs_path("guides/demo.md")
|
||||
|
||||
assert path == PurePosixPath("guides/demo.md")
|
||||
assert isinstance(path, PurePosixPath)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
(
|
||||
"/absolute.md",
|
||||
"../outside.md",
|
||||
"guides\\demo.md",
|
||||
"guides//demo.md",
|
||||
"guides/demo.txt",
|
||||
),
|
||||
)
|
||||
def test_parse_docs_path_rejects_invalid_paths(self, value: str) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
parse_docs_path(value)
|
||||
@@ -1,27 +0,0 @@
|
||||
from pathlib import Path
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.registry.load import load_markdown
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
class TestLoadMarkdown:
|
||||
"""Covers recursive Markdown discovery and loading."""
|
||||
|
||||
def test_loads_markdown_in_stable_order(self, tmp_path: Path) -> None:
|
||||
nested = tmp_path / "guides"
|
||||
nested.mkdir()
|
||||
(nested / "b.md").write_text("caf\u00e9\n", encoding="utf-8")
|
||||
(nested / "a.md").write_text("alpha\n", encoding="utf-8")
|
||||
(nested / "ignored.txt").write_text("ignored\n", encoding="utf-8")
|
||||
|
||||
docs = load_markdown(tmp_path)
|
||||
|
||||
assert list(docs) == [
|
||||
PurePosixPath("guides/a.md"),
|
||||
PurePosixPath("guides/b.md"),
|
||||
]
|
||||
assert docs[PurePosixPath("guides/b.md")] == "caf\u00e9\n"
|
||||
@@ -1,37 +0,0 @@
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.registry.load import read_docs_markdown_path
|
||||
from personal_mcp.registry.models import DocsRegistry
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def _make_registry() -> DocsRegistry:
|
||||
index_path = PurePosixPath("index.md")
|
||||
return DocsRegistry(
|
||||
docs_markdown_by_path={index_path: "# index"},
|
||||
docs_markdown_path_index=(index_path,),
|
||||
)
|
||||
|
||||
|
||||
def test_reads_docs_path_from_string_boundary() -> None:
|
||||
payload = read_docs_markdown_path(_make_registry(), "index.md")
|
||||
|
||||
assert payload == {
|
||||
"uri": "resource://docs/index.md",
|
||||
"format": "markdown",
|
||||
"source_path": "docs/index.md",
|
||||
"content": "# index",
|
||||
}
|
||||
|
||||
|
||||
def test_rejects_skill_docs_path() -> None:
|
||||
with pytest.raises(KeyError, match="unknown docs path"):
|
||||
read_docs_markdown_path(_make_registry(), "skills/demo/SKILL.md")
|
||||
|
||||
|
||||
def test_rejects_non_posix_docs_path() -> None:
|
||||
with pytest.raises(ValueError, match="POSIX separators"):
|
||||
read_docs_markdown_path(_make_registry(), "guides\\demo.md")
|
||||
@@ -1,67 +0,0 @@
|
||||
from importlib.resources import files
|
||||
from importlib.resources.abc import Traversable
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from fastmcp import Client
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.utilities.skills import get_skill_manifest
|
||||
from fastmcp.utilities.skills import list_skills
|
||||
|
||||
from personal_mcp.skills import create_skills_provider
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
SKILLS_ROOT = files("personal_mcp").joinpath("docs", "skills")
|
||||
|
||||
|
||||
def skill_directories() -> list[Traversable]:
|
||||
return [
|
||||
directory
|
||||
for directory in SKILLS_ROOT.iterdir()
|
||||
if directory.is_dir() and directory.joinpath("SKILL.md").is_file()
|
||||
]
|
||||
|
||||
|
||||
class TestSkillsProvider:
|
||||
"""Covers native FastMCP skill discovery and retrieval."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discovers_authored_skills(self) -> None:
|
||||
"""Ensures each authored skill is exposed with its description."""
|
||||
expected_names = {directory.name for directory in skill_directories()}
|
||||
mcp = FastMCP("skills-test")
|
||||
mcp.add_provider(create_skills_provider())
|
||||
|
||||
async with Client(mcp) as client:
|
||||
skills = await list_skills(client)
|
||||
|
||||
assert {skill.name for skill in skills} == expected_names
|
||||
assert all(skill.description for skill in skills)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reads_manifest_and_supporting_file(self) -> None:
|
||||
"""Ensures manifests disclose hashed files that remain directly readable."""
|
||||
mcp = FastMCP("skills-test")
|
||||
mcp.add_provider(create_skills_provider())
|
||||
|
||||
async with Client(mcp) as client:
|
||||
manifest = await get_skill_manifest(client, "mcp-details")
|
||||
reference = next(file for file in manifest.files if file.path.startswith("references/"))
|
||||
contents = await client.read_resource(f"skill://mcp-details/{reference.path}")
|
||||
|
||||
assert any(file.path == "SKILL.md" for file in manifest.files)
|
||||
assert all(file.hash.startswith("sha256:") for file in manifest.files)
|
||||
assert reference.size > 0
|
||||
assert contents
|
||||
|
||||
def test_frontmatter_names_match_directories(self) -> None:
|
||||
"""Ensures provider identity and authored skill names remain aligned."""
|
||||
for directory in skill_directories():
|
||||
skill_file = directory.joinpath("SKILL.md")
|
||||
raw = skill_file.read_text(encoding="utf-8")
|
||||
frontmatter = yaml.safe_load(raw.split("---", 2)[1])
|
||||
|
||||
assert set(frontmatter) == {"name", "description"}
|
||||
assert frontmatter["name"] == directory.name
|
||||
assert frontmatter["description"]
|
||||
@@ -1,55 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import ASGITransport
|
||||
from httpx import AsyncClient
|
||||
from httpx2 import ASGITransport as McpASGITransport
|
||||
from httpx2 import AsyncClient as McpAsyncClient
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
|
||||
from personal_mcp.web.app import create_app
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client() -> AsyncGenerator[AsyncClient]:
|
||||
"""Provides an AsyncClient bound to a fresh application instance."""
|
||||
app = create_app()
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app),
|
||||
base_url="http://testserver",
|
||||
timeout=10.0,
|
||||
) as test_client:
|
||||
yield test_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mcp_session_factory():
|
||||
"""Provides an in-process context manager factory for MCP SDK sessions."""
|
||||
|
||||
@asynccontextmanager
|
||||
async def create_session(*, initialize: bool = True) -> AsyncGenerator[ClientSession]:
|
||||
app = create_app()
|
||||
mcp_url = f"http://testserver{app.state.settings.mounts.mcp}"
|
||||
async with (
|
||||
app.router.lifespan_context(app),
|
||||
McpAsyncClient(
|
||||
transport=McpASGITransport(app=app),
|
||||
base_url="http://testserver",
|
||||
timeout=10.0,
|
||||
) as http_client,
|
||||
streamable_http_client(
|
||||
mcp_url,
|
||||
http_client=http_client,
|
||||
) as (read_stream, write_stream),
|
||||
ClientSession(read_stream, write_stream) as session,
|
||||
):
|
||||
if initialize:
|
||||
await session.initialize()
|
||||
yield session
|
||||
|
||||
return create_session
|
||||
@@ -1,58 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
pytestmark = pytest.mark.smoke
|
||||
|
||||
|
||||
class TestMcpHttpEndpoints:
|
||||
"""Covers smoke-level HTTP checks for mounted MCP runtime endpoints."""
|
||||
|
||||
class TestHealthz:
|
||||
"""Covers health endpoint smoke behavior."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_ok_payload(self, client: AsyncClient) -> None:
|
||||
"""Ensures GET /healthz responds with a healthy status payload."""
|
||||
response = await client.get("/healthz")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
|
||||
class TestDocsRoute:
|
||||
"""Covers static docs route smoke behavior."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_serves_docs_entrypoint(self, client: AsyncClient) -> None:
|
||||
"""Ensures GET /docs returns the docs site entrypoint response."""
|
||||
response = await client.get("/docs", follow_redirects=True)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "text/html" in response.headers["content-type"]
|
||||
|
||||
class TestMcpRoute:
|
||||
"""Covers MCP transport endpoint smoke behavior."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_does_not_publish_legacy_resource_bridge_tools(self, mcp_session_factory) -> None:
|
||||
"""Ensures deprecated compatibility tools are not exposed on the MCP route."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
tools_result = await mcp_session.list_tools()
|
||||
|
||||
tool_names = {tool.name for tool in tools_result.tools}
|
||||
assert "search_skills" not in tool_names
|
||||
assert "list_resources" not in tool_names
|
||||
assert "read_resource" not in tool_names
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_accepts_initialize_jsonrpc_request(
|
||||
self,
|
||||
mcp_session_factory,
|
||||
) -> None:
|
||||
"""Ensures POST /mcp accepts an initialize JSON-RPC request."""
|
||||
async with mcp_session_factory(initialize=False) as mcp_session_uninitialized:
|
||||
initialize_result = await mcp_session_uninitialized.initialize()
|
||||
|
||||
assert initialize_result.protocol_version
|
||||
assert initialize_result.server_info.name
|
||||
@@ -1,79 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from mcp_types import PromptReference
|
||||
|
||||
pytestmark = pytest.mark.smoke
|
||||
|
||||
EXPECTED_PROMPTS = {
|
||||
"authoring",
|
||||
"greenfield-architecture",
|
||||
"jsfiddle-page-layout",
|
||||
"mcp-consumer-repo-shim",
|
||||
"nicegui-component-extraction",
|
||||
"pytest-fill-scaffold",
|
||||
"pytest-scaffold",
|
||||
}
|
||||
|
||||
|
||||
class TestMcpPromptSurface:
|
||||
"""Covers smoke-level MCP prompt discovery and retrieval paths."""
|
||||
|
||||
class TestServerMetadata:
|
||||
"""Covers client-facing identity and completion capabilities."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_advertises_instructions_and_completions(self, mcp_session_factory) -> None:
|
||||
"""Ensures VS Code receives server guidance and completion support."""
|
||||
async with mcp_session_factory(initialize=False) as mcp_session:
|
||||
result = await mcp_session.initialize()
|
||||
|
||||
assert result.instructions is not None
|
||||
assert "skill://<name>/SKILL.md" in result.instructions
|
||||
assert result.server_info.icons
|
||||
assert result.server_info.icons[0].src.startswith("data:image/svg+xml;base64,")
|
||||
assert result.capabilities.completions is not None
|
||||
|
||||
class TestPromptDiscovery:
|
||||
"""Covers MCP prompts/list behavior using native prompt objects."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lists_prompt_objects(self, mcp_session_factory) -> None:
|
||||
"""Ensures prompts/list returns native prompt objects with stable names."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.list_prompts()
|
||||
|
||||
assert {prompt.name for prompt in result.prompts} == EXPECTED_PROMPTS
|
||||
assert all(prompt.description for prompt in result.prompts)
|
||||
assert all(prompt.title for prompt in result.prompts)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_completes_authored_prompt_choices(self, mcp_session_factory) -> None:
|
||||
"""Ensures finite prompt choices are available as IDE-style suggestions."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.complete(
|
||||
PromptReference(type="ref/prompt", name="authoring"),
|
||||
{"name": "artifact_type", "value": "pr"},
|
||||
)
|
||||
|
||||
assert result.completion.values == ["prompt"]
|
||||
|
||||
class TestPromptResolution:
|
||||
"""Covers MCP prompts/get behavior using native request and response objects."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gets_prompt_as_native_object(self, mcp_session_factory) -> None:
|
||||
"""Ensures prompts/get resolves a listed prompt into structured message objects."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
resolved_prompt = await mcp_session.get_prompt(
|
||||
name="authoring",
|
||||
arguments={
|
||||
"artifact_type": "skill",
|
||||
"artifact_id": "demo-skill",
|
||||
"goal": "Demonstrate native prompt rendering.",
|
||||
},
|
||||
)
|
||||
|
||||
assert resolved_prompt.messages
|
||||
assert all(message.content for message in resolved_prompt.messages)
|
||||
assert "`artifact_id`: demo-skill" in resolved_prompt.messages[0].content.text
|
||||
@@ -1,59 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.smoke
|
||||
|
||||
|
||||
class TestMcpSkillsSurface:
|
||||
"""Covers native skill resources over the HTTP MCP surface."""
|
||||
|
||||
class TestTools:
|
||||
"""Covers absence of deprecated compatibility tools."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_does_not_expose_resource_bridge_tools(self, mcp_session_factory) -> None:
|
||||
"""Ensures native resources are not projected through legacy compatibility tools."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.list_tools()
|
||||
|
||||
tool_names = {tool.name for tool in result.tools}
|
||||
assert "search_skills" not in tool_names
|
||||
assert "list_resources" not in tool_names
|
||||
assert "read_resource" not in tool_names
|
||||
|
||||
class TestResources:
|
||||
"""Covers native skill resources, manifests, and file templates."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lists_main_file_and_manifest(self, mcp_session_factory) -> None:
|
||||
"""Ensures resources/list exposes native skill entry points."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.list_resources()
|
||||
resource_uris = {str(resource.uri) for resource in result.resources}
|
||||
|
||||
assert "skill://mcp-details/SKILL.md" in resource_uris
|
||||
assert "skill://mcp-details/_manifest" in resource_uris
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lists_resource_templates(self, mcp_session_factory) -> None:
|
||||
"""Ensures supporting files use per-skill wildcard templates."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
result = await mcp_session.list_resource_templates()
|
||||
template_uris = {template.uri_template for template in result.resource_templates}
|
||||
|
||||
assert "skill://mcp-details/{path*}" in template_uris
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reads_manifest_and_supporting_file(self, mcp_session_factory) -> None:
|
||||
"""Ensures manifest paths resolve through the supporting-file template."""
|
||||
async with mcp_session_factory() as mcp_session:
|
||||
manifest_result = await mcp_session.read_resource("skill://mcp-details/_manifest")
|
||||
manifest = json.loads(manifest_result.contents[0].text)
|
||||
reference = next(file["path"] for file in manifest["files"] if file["path"].startswith("references/"))
|
||||
reference_result = await mcp_session.read_resource(f"skill://mcp-details/{reference}")
|
||||
|
||||
assert manifest["skill"] == "mcp-details"
|
||||
assert reference_result.contents
|
||||
Reference in New Issue
Block a user